Passed
Push — master ( 0524f3...409314 )
by Edward
05:43
created

BuildLookupTableCommand::getSpecFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 9
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Remorhaz\UniLex\Console;
6
7
use PhpParser\BuilderFactory;
8
use PhpParser\Comment\Doc;
9
use PhpParser\Node\Expr\Array_;
10
use PhpParser\Node\Expr\ArrayItem;
11
use PhpParser\Node\Stmt\Declare_;
12
use PhpParser\Node\Stmt\DeclareDeclare;
13
use PhpParser\Node\Stmt\Return_;
14
use PhpParser\PrettyPrinterAbstract;
15
use ReflectionClass;
16
use ReflectionException;
17
use Remorhaz\UniLex\Exception as UniLexException;
18
use Remorhaz\UniLex\Grammar\ContextFree\GrammarLoader;
19
use Remorhaz\UniLex\Parser\LL1\Lookup\TableBuilder;
20
use RuntimeException;
21
use Symfony\Component\Console\Command\Command;
22
use Symfony\Component\Console\Exception\InvalidOptionException;
23
use Symfony\Component\Console\Input\InputArgument;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Input\InputOption;
26
use Symfony\Component\Console\Output\OutputInterface;
27
28
use function array_flip;
29
use function realpath;
30
use function sort;
31
32
use const SORT_ASC;
33
use const SORT_STRING;
34
35
final class BuildLookupTableCommand extends Command
36
{
37
38
    private const TYPE_LL_1 = 'll-1';
39
40
    protected static $defaultName = 'build-lookup-table';
41
42
    private $printer;
43
44
    private $builder;
45
46
    public function __construct(PrettyPrinterAbstract $printer, string $name = null)
47
    {
48
        parent::__construct($name);
49
        $this->printer = $printer;
50
        $this->builder = new BuilderFactory();
51
    }
52
53
    protected function configure()
54
    {
55
        $this
56
            ->setDescription('Builds parser lookup table.')
57
            ->addArgument('spec', InputArgument::REQUIRED, 'Grammar specification file')
58
            ->addArgument('target', InputArgument::REQUIRED, 'Target file')
59
            ->addOption('type', 't', InputOption::VALUE_REQUIRED, 'Parser type', self::TYPE_LL_1)
60
            ->addOption('symbol', null, InputOption::VALUE_REQUIRED, 'Class with symbol constants')
61
            ->addOption('token', null, InputOption::VALUE_REQUIRED, 'Class with token constants')
62
            ->addOption('desc', 'd', InputOption::VALUE_REQUIRED, 'Lookup table description');
63
    }
64
65
    /**
66
     * @param InputInterface  $input
67
     * @param OutputInterface $output
68
     * @return int
69
     * @throws UniLexException
70
     */
71
    protected function execute(InputInterface $input, OutputInterface $output)
72
    {
73
        $output->writeln('Building parser lookup table...');
74
        $map = $this->buildMap($input, $output);
75
        $uses = [];
76
77
        $symbols = [];
78
        $symbolsClass = $this->findSymbols($input);
79
        if (isset($symbolsClass)) {
80
            $output->writeln("Symbols: {$symbolsClass->getName()}");
81
            $uses[] = $symbolsClass->getName();
82
            $symbols = (array) array_flip($symbolsClass->getConstants());
83
        }
84
85
        $tokens = [];
86
        $tokensClass = $this->findTokens($input);
87
        if (isset($tokensClass)) {
88
            $output->writeln("Tokens: {$tokensClass->getName()}");
89
            $uses[] = $tokensClass->getName();
90
            $tokens = (array) array_flip($tokensClass->getConstants());
91
        }
92
93
        $description = $this->findDescription($input);
94
        $descriptionText = isset($description)
95
            ? "{$description}\n * \n * "
96
            : '';
97
98
        $nodes = [];
99
100
        $fileDocText = <<<EOF
101
/**
102
 * {$descriptionText}Auto-generated file, please don't edit manually.
103
 * Generated by UniLex.
104
 */
105
EOF;
106
107
        $declare =  new Declare_([new DeclareDeclare('strict_types', $this->builder->val(1))]);
108
        $declare->setDocComment(new Doc($fileDocText));
109
        $nodes[] = $declare;
110
111
        sort($uses, SORT_STRING | SORT_ASC);
112
        foreach ($uses as $index => $use) {
113
            $nodes[] = $this->builder->use($use)->getNode();
114
        }
115
116
        $items = [];
117
        foreach ($map as $symbol => $productions) {
118
            $productionItems = [];
119
            foreach ($productions as $token => $production) {
120
                $productionItems[] = new ArrayItem(
121
                    $this->builder->val($production),
122
                    isset($tokens[$token])
123
                        ? $this->builder->classConstFetch($tokensClass->getShortName(), $tokens[$token])
124
                        : $this->builder->val($token)
125
                );
126
            }
127
            $items[] = new ArrayItem(
128
                new Array_($productionItems, ['kind' => Array_::KIND_SHORT]),
129
                isset($symbols[$symbol])
130
                    ? $this->builder->classConstFetch($symbolsClass->getShortName(), $symbols[$symbol])
131
                    : $this->builder->val($symbol)
132
            );
133
        }
134
135
        $nodes[] = new Return_(
136
            new Array_($items, ['kind' => Array_::KIND_SHORT])
137
        );
138
139
        $generatedCode = $this->printer->prettyPrintFile($nodes);
140
        $this->buildTarget($input, $output, $generatedCode);
141
142
        return 0;
143
    }
144
145
    private function findDescription(InputInterface $input): ?string
146
    {
147
        return $input->getOption('desc');
148
    }
149
150
    /**
151
     * @param InputInterface  $input
152
     * @param OutputInterface $output
153
     * @return array
154
     * @throws UniLexException
155
     */
156
    private function buildMap(InputInterface $input, OutputInterface $output): array
157
    {
158
        $specFile = $this->getSpecFile($input);
159
        $output->writeln("Specification used: {$specFile}");
160
        $grammar = GrammarLoader::loadFile($specFile);
161
162
        return (new TableBuilder($grammar))
163
            ->getTable()
164
            ->exportMap();
165
    }
166
167
    private function getSpecFile(InputInterface $input): string
168
    {
169
        $specFile = $input->getArgument('spec');
170
        $specFile = realpath($specFile);
0 ignored issues
show
Bug introduced by
It seems like $specFile can also be of type string[]; however, parameter $path of realpath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

170
        $specFile = realpath(/** @scrutinizer ignore-type */ $specFile);
Loading history...
171
        if (false === $specFile) {
172
            throw new InvalidOptionException("Argument #1 must contain valid path to specification file");
173
        }
174
175
        return $specFile;
176
    }
177
178
    private function findSymbols(InputInterface $input): ?ReflectionClass
179
    {
180
        $className = $input->getOption('symbol');
181
        if (!isset($className)) {
182
            return null;
183
        }
184
185
        try {
186
            return new ReflectionClass($className);
187
        } catch (ReflectionException $e) {
188
            throw new InvalidOptionException("Option --symbol must contain valid PHP class", 0, $e);
189
        }
190
    }
191
192
    private function findTokens(InputInterface $input): ?ReflectionClass
193
    {
194
        $className = $input->getOption('token');
195
        if (!isset($className)) {
196
            return null;
197
        }
198
199
        try {
200
            return new ReflectionClass($className);
201
        } catch (ReflectionException $e) {
202
            throw new InvalidOptionException("Option --token must contain valid PHP class", 0, $e);
203
        }
204
    }
205
206
    private function getTargetFile(InputInterface $input): string
207
    {
208
        return $input->getArgument('target');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $input->getArgument('target') could return the type null|string[] which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
209
    }
210
211
    private function buildTarget(InputInterface $input, OutputInterface $output, string $content): void
212
    {
213
        $targetFile = $this->getTargetFile($input);
214
        $output->writeln("Saving generated data to file {$targetFile}...");
215
216
        if (false === file_put_contents($targetFile, $content)) {
217
            throw new RuntimeException("Failed to write file {$targetFile}");
218
        }
219
        $byteCount = strlen($content);
220
        $output->writeln("Done ({$byteCount} bytes)!");
221
    }
222
}
223