Passed
Push — master ( 77aa03...453f7d )
by Nikita
02:16
created

Template::parseTemplate()   D

Complexity

Conditions 10
Paths 12

Size

Total Lines 31
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 23
nc 12
nop 1
dl 0
loc 31
rs 4.8196
c 0
b 0
f 0

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 PHP-Yacc package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace PhpYacc\CodeGen;
11
12
use PhpYacc\Compress\Compress;
13
use PhpYacc\Compress\CompressResult;
14
use PhpYacc\Exception\LogicException;
15
use PhpYacc\Exception\TemplateException;
16
use PhpYacc\Grammar\Context;
17
use PhpYacc\Support\Utils;
18
use PhpYacc\Yacc\Macro\DollarExpansion;
19
20
class Template
21
{
22
    /**
23
     * @var string
24
     */
25
    protected $metaChar = '$';
26
27
    /**
28
     * @var array
29
     */
30
    protected $template = [];
31
32
    /**
33
     * @var int
34
     */
35
    protected $lineNumber = 0;
36
37
    /**
38
     * @var bool
39
     */
40
    protected $copyHeader = false;
41
42
    /**
43
     * @var Context
44
     */
45
    protected $context;
46
47
    /**
48
     * @var CompressResult
49
     */
50
    protected $compress;
51
52
    /**
53
     * @var Language
54
     */
55
    protected $language;
56
57
    /**
58
     * Template constructor.
59
     *
60
     * @param Language $language
61
     * @param string   $template
62
     * @param Context  $context
63
     *
64
     * @throws TemplateException
65
     */
66
    public function __construct(Language $language, string $template, Context $context)
67
    {
68
        $this->language = $language;
69
        $this->context = $context;
70
71
        $this->parseTemplate($template);
72
    }
73
74
    /**
75
     * @param CompressResult $result
76
     * @param $resultFile
77
     * @param null $headerFile
78
     *
79
     * @throws LogicException
80
     * @throws TemplateException
81
     */
82
    public function render(CompressResult $result, $resultFile, $headerFile = null)
83
    {
84
        $headerFile = $headerFile ?: \fopen('php://memory', 'rw');
85
86
        $this->language->begin($resultFile, $headerFile);
87
88
        $this->compress = $result;
89
        unset($result);
90
91
        $skipMode = false;
92
        $lineChanged = false;
93
        $tailCode = false;
94
        $reduceMode = [
95
            'enabled' => false,
96
            'm'       => -1,
97
            'n'       => 0,
98
            'mac'     => [],
99
        ];
100
        $tokenMode = [
101
            'enabled' => false,
102
            'mac'     => [],
103
        ];
104
        $buffer = '';
105
106
        foreach ($this->template as $line) {
107
            $line .= "\n";
108
            if ($tailCode) {
109
                $this->language->write($buffer.$line);
110
                continue;
111
            }
112
113
            if ($skipMode) {
114
                if ($this->metaMatch(\ltrim($line), 'endif')) {
115
                    $skipMode = false;
116
                }
117
                continue;
118
            }
119
120
            if ($reduceMode['enabled']) {
121
                if ($this->metaMatch(\trim($line), 'endreduce')) {
122
                    $reduceMode['enabled'] = false;
123
                    $this->lineNumber++;
124
                    if ($reduceMode['m'] < 0) {
125
                        $reduceMode['m'] = $reduceMode['n'];
126
                    }
127
128
                    foreach ($this->context->grams as $gram) {
129
                        if ($gram->action) {
130
                            for ($j = 0; $j < $reduceMode['m']; $j++) {
131
                                $this->expandMacro($reduceMode['mac'][$j], $gram->num, null);
132
                            }
133
                        } else {
134
                            for ($j = $reduceMode['m']; $j < $reduceMode['n']; $j++) {
135
                                $this->expandMacro($reduceMode['mac'][$j], $gram->num, null);
136
                            }
137
                        }
138
                    }
139
                    continue;
140
                } elseif ($this->metaMatch(\trim($line), 'noact')) {
141
                    $reduceMode['m'] = $reduceMode['n'];
142
                    continue;
143
                }
144
                $reduceMode['mac'][$reduceMode['n']++] = $line;
145
                continue;
146
            }
147
148
            if ($tokenMode['enabled']) {
149
                if ($this->metaMatch(\trim($line), 'endtokenval')) {
150
                    $tokenMode['enabled'] = false;
151
                    $this->lineNumber++;
152
                    for ($i = 1; $i < $this->context->countTerminals; $i++) {
153
                        $symbol = $this->context->symbol($i);
154
                        if ($symbol->name[0] != '\'') {
155
                            $str = $symbol->name;
156
                            if ($i === 1) {
157
                                $str = 'YYERRTOK';
158
                            }
159
                            foreach ($tokenMode['mac'] as $mac) {
160
                                $this->expandMacro($mac, $symbol->value, $str);
161
                            }
162
                        }
163
                    }
164
                } else {
165
                    $tokenMode['mac'][] = $line;
166
                }
167
                continue;
168
            }
169
            $p = $line;
170
            $buffer = '';
171
            for ($i = 0; $i < \mb_strlen($line); $i++) {
172
                $p = \mb_substr($line, $i);
173
                if ($p[0] !== $this->metaChar) {
174
                    $buffer .= $line[$i];
175
                } elseif ($i + 1 < \mb_strlen($line) && $p[1] === $this->metaChar) {
176
                    $i++;
177
                    $buffer .= $this->metaChar;
178
                } elseif ($this->metaMatch($p, '(')) {
179
                    $start = $i + 2;
180
                    $val = \mb_substr($p, 2);
181
                    while ($i < \mb_strlen($line) && $line[$i] !== ')') {
182
                        $i++;
183
                    }
184
                    if (!isset($line[$i])) {
185
                        throw new TemplateException('$(: missing ")"');
186
                    }
187
                    $length = $i - $start;
188
189
                    $buffer .= $this->genValueOf(\mb_substr($val, 0, $length));
190
                } elseif ($this->metaMatch($p, 'TYPEOF(')) {
191
                    throw new LogicException('TYPEOF is not implemented');
192
                } else {
193
                    break;
194
                }
195
            }
196
            if (isset($p[0]) && $p[0] === $this->metaChar) {
197
                if (\trim($buffer) !== '') {
198
                    throw new TemplateException('Non-blank character before $-keyword');
199
                }
200
                if ($this->metaMatch($p, 'header')) {
201
                    $this->copyHeader = true;
202
                } elseif ($this->metaMatch($p, 'endheader')) {
203
                    $this->copyHeader = false;
204
                } elseif ($this->metaMatch($p, 'tailcode')) {
205
                    //$this->printLine();
0 ignored issues
show
Unused Code Comprehensibility introduced by
84% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
206
                    $tailCode = true;
207
                    continue;
208
                } elseif ($this->metaMatch($p, 'verification-table')) {
209
                    throw new TemplateException('verification-table is not implemented');
210
                } elseif ($this->metaMatch($p, 'union')) {
211
                    throw new TemplateException('union is not implemented');
212
                } elseif ($this->metaMatch($p, 'tokenval')) {
213
                    $tokenMode = [
214
                        'enabled' => true,
215
                        'mac'     => [],
216
                    ];
217
                } elseif ($this->metaMatch($p, 'reduce')) {
218
                    $reduceMode = [
219
                        'enabled' => true,
220
                        'm'       => -1,
221
                        'n'       => 0,
222
                        'mac'     => [],
223
                    ];
224
                } elseif ($this->metaMatch($p, 'switch-for-token-name')) {
225
                    for ($i = 0; $i < $this->context->countTerminals; $i++) {
226
                        if ($this->context->cTermIndex[$i] >= 0) {
227
                            $symbol = $this->context->symbol($i);
228
                            $this->language->caseBlock($buffer, $symbol->value, $symbol->name);
229
                        }
230
                    }
231
                } elseif ($this->metaMatch($p, 'production-strings')) {
232
                    foreach ($this->context->grams as $gram) {
233
                        $info = \array_slice($gram->body, 0);
234
                        $this->language->write($buffer.'"');
235
                        $this->language->writeQuoted($info[0]->name);
236
                        $this->language->writeQuoted(' :');
237
                        if (\count($info) === 1) {
238
                            $this->language->writeQuoted(' /* empty */');
239
                        }
240
                        for ($i = 1; $i < \count($info); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
241
                            $this->language->writeQuoted(' '.$info[$i]->name);
242
                        }
243
                        if ($gram->num + 1 === $this->context->countGrams) {
244
                            $this->language->write("\"\n");
245
                        } else {
246
                            $this->language->write("\",\n");
247
                        }
248
                    }
249
                } elseif ($this->metaMatch($p, 'listvar')) {
250
                    $var = \trim(\mb_substr($p, 9));
251
                    $this->genListVar($buffer, $var);
252
                } elseif ($this->metaMatch($p, 'ifnot')) {
253
                    $skipMode = $skipMode || !$this->skipIf($p);
254
                } elseif ($this->metaMatch($p, 'if')) {
255
                    $skipMode = $skipMode || $this->skipIf($p);
256
                } elseif ($this->metaMatch($p, 'endif')) {
257
                    $skipMode = false;
258
                } else {
259
                    throw new TemplateException("Unknown \$: $line");
260
                }
261
                $lineChanged = true;
262
            } else {
263
                if ($lineChanged) {
264
                    //$this->printLine();
0 ignored issues
show
Unused Code Comprehensibility introduced by
84% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
265
                    $lineChanged = false;
266
                }
267
                $this->language->write($buffer, $this->copyHeader);
268
            }
269
        }
270
271
        $this->language->commit();
272
    }
273
274
    /**
275
     * @param $spec string
276
     *
277
     * @throws TemplateException
278
     *
279
     * @return bool
280
     */
281
    protected function skipIf(string $spec): bool
282
    {
283
        [ $dump, $test ] = \explode(' ', $spec, 2);
0 ignored issues
show
Bug introduced by
The variable $dump does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $test seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
284
285
        $test = \trim($test);
0 ignored issues
show
Bug introduced by
The variable $test seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
286
        switch ($test) {
287
            case '-a':
288
                return $this->context->aflag;
289
290
            case '-t':
291
                return $this->context->tflag;
292
293
            case '-p':
294
                return (bool) $this->context->className;
295
296
            case '%union':
297
                return (bool) $this->context->unioned;
298
299
            case '%pure_parser':
300
                return $this->context->pureFlag;
301
302
            default:
303
                throw new TemplateException("$dump: unknown switch: $test");
304
        }
305
    }
306
307
    /**
308
     * @param string      $def
309
     * @param int         $value
310
     * @param string|null $str
311
     */
312
    protected function expandMacro(string $def, int $value, string $str = null)
313
    {
314
        $result = '';
315
        for ($i = 0; $i < \mb_strlen($def); $i++) {
316
            $p = $def[$i];
317
            if ($p === '%') {
318
                $p = $def[++$i];
319
                switch ($p) {
320
                    case 'n':
321
                        $result .= \sprintf('%d', $value);
322
                        break;
323
324
                    case 's':
325
                        $result .= $str !== null ? $str : '';
326
                        break;
327
328
                    case 'b':
329
                        $gram = $this->context->gram($value);
330
                        $this->printLine($gram->position);
331
                        $result .= $gram->action;
332
                        break;
333
334
                    default:
335
                        $result .= $p;
336
                        break;
337
                }
338
            } else {
339
                $result .= $p;
340
            }
341
        }
342
343
        $this->language->write($result, $this->copyHeader);
344
    }
345
346
    /**
347
     * @param string $indent
348
     * @param string $var
349
     *
350
     * @throws TemplateException
351
     */
352
    protected function genListVar(string $indent, string $var)
353
    {
354
        $size = -1;
355
        if (isset($this->compress->$var)) {
356
            $array = $this->compress->$var;
357
            if (isset($this->compress->{$var.'size'})) {
358
                $size = $this->compress->{$var.'size'};
359
            } elseif ($var === 'yydefault') {
360
                $size = $this->context->countNonLeafStates;
361
            } elseif (\in_array($var, ['yygbase', 'yygdefault'])) {
362
                $size = $this->context->countNonTerminals;
363
            } elseif (\in_array($var, ['yylhs', 'yylen'])) {
364
                $size = $this->context->countGrams;
365
            }
366
            $this->printArray($array, $size < 0 ? count($array) : $size, $indent);
367
        } elseif ($var === 'terminals') {
368
            $nl = 0;
369
            foreach ($this->context->terminals as $term) {
370
                if ($this->context->cTermIndex[$term->code] >= 0) {
371
                    $prefix = $nl++ ? ",\n" : '';
372
                    $this->language->write($prefix.$indent.'"');
373
                    $this->language->writeQuoted($term->name);
374
                    $this->language->write('"');
375
                }
376
            }
377
            $this->language->write("\n");
378
        } elseif ($var === 'nonterminals') {
379
            $nl = 0;
380
            foreach ($this->context->nonterminals as $nonterm) {
381
                $prefix = $nl++ ? ",\n" : '';
382
                $this->language->write($prefix.$indent.'"');
383
                $this->language->writeQuoted($nonterm->name);
384
                $this->language->write('"');
385
            }
386
            $this->language->write("\n");
387
        } else {
388
            throw new TemplateException("\$listvar: unknown variable $var");
389
        }
390
    }
391
392
    /**
393
     * @param array  $array
394
     * @param int    $limit
395
     * @param string $indent
396
     */
397
    protected function printArray(array $array, int $limit, string $indent)
398
    {
399
        $col = 0;
400
        for ($i = 0; $i < $limit; $i++) {
401
            if ($col === 0) {
402
                $this->language->write($indent);
403
            }
404
            $this->language->write(\sprintf($i + 1 === $limit ? '%5d' : '%5d,', $array[$i]));
405
            if (++$col === 10) {
406
                $this->language->write("\n");
407
                $col = 0;
408
            }
409
        }
410
        if ($col !== 0) {
411
            $this->language->write("\n");
412
        }
413
    }
414
415
    /**
416
     * @param string $var
417
     *
418
     * @throws TemplateException
419
     *
420
     * @return string
421
     */
422
    protected function genValueOf(string $var): string
423
    {
424
        switch ($var) {
425
            case 'YYSTATES':
426
                return \sprintf('%d', $this->context->countStates);
427
            case 'YYNLSTATES':
428
                return \sprintf('%d', $this->context->countNonLeafStates);
429
            case 'YYINTERRTOK':
430
                return \sprintf('%d', $this->compress->yytranslate[$this->context->errorToken->value]);
431
            case 'YYUNEXPECTED':
432
                return \sprintf('%d', Compress::UNEXPECTED);
433
            case 'YYDEFAULT':
434
                return \sprintf('%d', Compress::DEFAULT);
435
            case 'YYMAXLEX':
436
                return \sprintf('%d', \count($this->compress->yytranslate));
437
            case 'YYLAST':
438
                return \sprintf('%d', \count($this->compress->yyaction));
439
            case 'YYGLAST':
440
                return \sprintf('%d', \count($this->compress->yygoto));
441
            case 'YYTERMS':
442
            case 'YYBADCH':
443
                return \sprintf('%d', $this->compress->yyncterms);
444
            case 'YYNONTERMS':
445
                return \sprintf('%d', $this->context->countNonTerminals);
446
            case 'YY2TBLSTATE':
447
                return \sprintf('%d', $this->compress->yybasesize - $this->context->countNonLeafStates);
448
            case 'CLASSNAME':
449
            case '-p':
450
                return $this->context->className ?: 'yy';
451
            default:
452
                throw new TemplateException("Unknown variable: \$($var)");
453
        }
454
    }
455
456
    /**
457
     * @param string $template
458
     *
459
     * @throws TemplateException
460
     */
461
    protected function parseTemplate(string $template)
462
    {
463
        $template = \preg_replace("(\r\n|\r)", "\n", $template);
464
        $lines = \explode("\n", $template);
465
        $this->lineNumber = 1;
466
        $skip = false;
467
468
        foreach ($lines as $line) {
469
            $p = $line;
470
            if ($skip) {
471
                $this->template[] = $line;
472
                continue;
473
            }
474
            while (\mb_strlen($p) > 0 && Utils::isWhite($p[0])) {
475
                $p = \mb_substr($p, 1);
476
            }
477
            $this->lineNumber++;
478
            if ($this->metaMatch($p, 'include')) {
479
                $skip = true;
480
            } elseif ($this->metaMatch($p, 'meta')) {
481
                if (!isset($p[6]) || Utils::isWhite($p[6])) {
482
                    throw new TemplateException("\$meta: missing character in definition: $p");
483
                }
484
                $this->metaChar = $p[6];
485
            } elseif ($this->metaMatch($p, 'semval')) {
486
                $this->defSemvalMacro(\mb_substr($p, 7));
487
            } else {
488
                $this->template[] = $line;
489
            }
490
        }
491
    }
492
493
    /**
494
     * @param string $text
495
     * @param string $keyword
496
     *
497
     * @return bool
498
     */
499
    protected function metaMatch(string $text, string $keyword): bool
500
    {
501
        return isset($text[0]) && $text[0] === $this->metaChar && \mb_substr($text, 1, \mb_strlen($keyword)) === $keyword;
502
    }
503
504
    /**
505
     * @param string $macro
506
     *
507
     * @throws TemplateException
508
     */
509
    protected function defSemvalMacro(string $macro)
510
    {
511
        if (\mb_strpos($macro, '($)') !== false) {
512
            $this->context->macros[DollarExpansion::SEMVAL_LHS_UNTYPED] = \ltrim(\mb_substr($macro, 3));
513
        } elseif (\mb_strpos($macro, '($,%t)') !== false) {
514
            $this->context->macros[DollarExpansion::SEMVAL_LHS_TYPED] = \ltrim(\mb_substr($macro, 6));
515
        } elseif (\mb_strpos($macro, '(%n)') !== false) {
516
            $this->context->macros[DollarExpansion::SEMVAL_RHS_UNTYPED] = \ltrim(\mb_substr($macro, 4));
517
        } elseif (\mb_strpos($macro, '(%n,%t)') !== false) {
518
            $this->context->macros[DollarExpansion::SEMVAL_RHS_TYPED] = \ltrim(\mb_substr($macro, 7));
519
        } else {
520
            throw new TemplateException("\$semval: bad format $macro");
521
        }
522
    }
523
524
    /**
525
     * @param int         $line
526
     * @param string|null $filename
527
     */
528
    protected function printLine(int $line = -1, string $filename = null)
529
    {
530
        $line = $line === -1 ? $this->lineNumber : $line;
531
        $filename = $filename ?? $this->context->filename;
532
533
        $this->language->inlineComment(\sprintf('%s:%d', $filename, $line));
534
    }
535
}
536