BuildLookupTableCommand::findTokens()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 11
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
    private const TYPE_LL_1 = 'll-1';
38
39
    protected static $defaultName = 'build-lookup-table';
40
41
    private $printer;
42
43
    private $builder;
44
45
    public function __construct(PrettyPrinterAbstract $printer, string $name = null)
46
    {
47
        parent::__construct($name);
48
        $this->printer = $printer;
49
        $this->builder = new BuilderFactory();
50
    }
51
52
    protected function configure()
53
    {
54
        $this
55
            ->setDescription('Builds parser lookup table.')
56
            ->addArgument('spec', InputArgument::REQUIRED, 'Grammar specification file')
57
            ->addArgument('target', InputArgument::REQUIRED, 'Target file')
58
            ->addOption('type', 't', InputOption::VALUE_REQUIRED, 'Parser type', self::TYPE_LL_1)
59
            ->addOption('symbol', null, InputOption::VALUE_REQUIRED, 'Class with symbol constants')
60
            ->addOption('token', null, InputOption::VALUE_REQUIRED, 'Class with token constants')
61
            ->addOption('desc', 'd', InputOption::VALUE_REQUIRED, 'Lookup table description');
62
    }
63
64
    /**
65
     * @param InputInterface  $input
66
     * @param OutputInterface $output
67
     * @return int
68
     * @throws UniLexException
69
     */
70
    protected function execute(InputInterface $input, OutputInterface $output)
71
    {
72
        $output->writeln('Building parser lookup table...');
73
        $map = $this->buildMap($input, $output);
74
        $uses = [];
75
76
        $symbols = [];
77
        $symbolsClass = $this->findSymbols($input);
78
        if (isset($symbolsClass)) {
79
            $output->writeln("Symbols: {$symbolsClass->getName()}");
80
            $uses[] = $symbolsClass->getName();
81
            $symbols = (array) array_flip($symbolsClass->getConstants());
82
        }
83
84
        $tokens = [];
85
        $tokensClass = $this->findTokens($input);
86
        if (isset($tokensClass)) {
87
            $output->writeln("Tokens: {$tokensClass->getName()}");
88
            $uses[] = $tokensClass->getName();
89
            $tokens = (array) array_flip($tokensClass->getConstants());
90
        }
91
92
        $description = $this->findDescription($input);
93
        $descriptionText = isset($description)
94
            ? "{$description}\n * \n * "
95
            : '';
96
97
        $nodes = [];
98
99
        $fileDocText = <<<EOF
100
/**
101
 * {$descriptionText}Auto-generated file, please don't edit manually.
102
 * Generated by UniLex.
103
 */
104
EOF;
105
106
        $declare =  new Declare_([new DeclareDeclare('strict_types', $this->builder->val(1))]);
107
        $declare->setDocComment(new Doc($fileDocText));
108
        $nodes[] = $declare;
109
110
        sort($uses, SORT_STRING | SORT_ASC);
111
        foreach ($uses as $index => $use) {
112
            $nodes[] = $this->builder->use($use)->getNode();
113
        }
114
115
        $items = [];
116
        foreach ($map as $symbol => $productions) {
117
            $productionItems = [];
118
            foreach ($productions as $token => $production) {
119
                $productionItems[] = new ArrayItem(
120
                    $this->builder->val($production),
121
                    isset($tokens[$token])
122
                        ? $this->builder->classConstFetch($tokensClass->getShortName(), $tokens[$token])
123
                        : $this->builder->val($token)
124
                );
125
            }
126
            $items[] = new ArrayItem(
127
                new Array_($productionItems, ['kind' => Array_::KIND_SHORT]),
128
                isset($symbols[$symbol])
129
                    ? $this->builder->classConstFetch($symbolsClass->getShortName(), $symbols[$symbol])
130
                    : $this->builder->val($symbol)
131
            );
132
        }
133
134
        $nodes[] = new Return_(
135
            new Array_($items, ['kind' => Array_::KIND_SHORT])
136
        );
137
138
        $generatedCode = $this->printer->prettyPrintFile($nodes);
139
        $this->buildTarget($input, $output, $generatedCode);
140
141
        return 0;
142
    }
143
144
    private function findDescription(InputInterface $input): ?string
145
    {
146
        return $input->getOption('desc');
147
    }
148
149
    /**
150
     * @param InputInterface  $input
151
     * @param OutputInterface $output
152
     * @return array
153
     * @throws UniLexException
154
     */
155
    private function buildMap(InputInterface $input, OutputInterface $output): array
156
    {
157
        $specFile = $this->getSpecFile($input);
158
        $output->writeln("Specification used: {$specFile}");
159
        $grammar = GrammarLoader::loadFile($specFile);
160
161
        return (new TableBuilder($grammar))
162
            ->getTable()
163
            ->exportMap();
164
    }
165
166
    private function getSpecFile(InputInterface $input): string
167
    {
168
        $specFile = $input->getArgument('spec');
169
        $specFile = realpath($specFile);
170
        if (false === $specFile) {
171
            throw new InvalidOptionException("Argument #1 must contain valid path to specification file");
172
        }
173
174
        return $specFile;
175
    }
176
177
    private function findSymbols(InputInterface $input): ?ReflectionClass
178
    {
179
        $className = $input->getOption('symbol');
180
        if (!isset($className)) {
181
            return null;
182
        }
183
184
        try {
185
            return new ReflectionClass($className);
186
        } catch (ReflectionException $e) {
187
            throw new InvalidOptionException("Option --symbol must contain valid PHP class", 0, $e);
188
        }
189
    }
190
191
    private function findTokens(InputInterface $input): ?ReflectionClass
192
    {
193
        $className = $input->getOption('token');
194
        if (!isset($className)) {
195
            return null;
196
        }
197
198
        try {
199
            return new ReflectionClass($className);
200
        } catch (ReflectionException $e) {
201
            throw new InvalidOptionException("Option --token must contain valid PHP class", 0, $e);
202
        }
203
    }
204
205
    private function getTargetFile(InputInterface $input): string
206
    {
207
        return $input->getArgument('target');
208
    }
209
210
    private function buildTarget(InputInterface $input, OutputInterface $output, string $content): void
211
    {
212
        $targetFile = $this->getTargetFile($input);
213
        $output->writeln("Saving generated data to file {$targetFile}...");
214
215
        if (false === file_put_contents($targetFile, $content)) {
216
            throw new RuntimeException("Failed to write file {$targetFile}");
217
        }
218
        $byteCount = strlen($content);
219
        $output->writeln("Done ({$byteCount} bytes)!");
220
    }
221
}
222