Completed
Push — master ( 539462...798097 )
by Maurício
16s queued 14s
created

ContextGenerator::buildAll()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 4
nop 2
dl 0
loc 19
ccs 0
cts 9
cp 0
crap 30
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Tools;
6
7
use function array_map;
8
use function array_merge;
9
use function array_slice;
10
use function basename;
11
use function count;
12
use function dirname;
13
use function file;
14
use function file_put_contents;
15
use function implode;
16
use function ksort;
17
use function preg_match;
18
use function round;
19
use function scandir;
20
use function sort;
21
use function sprintf;
22
use function str_repeat;
23
use function str_replace;
24
use function str_split;
25
use function strlen;
26
use function strstr;
27
use function strtoupper;
28
use function substr;
29
use function trim;
30
31
use const FILE_IGNORE_NEW_LINES;
32
use const FILE_SKIP_EMPTY_LINES;
33
use const SORT_STRING;
34
35
/**
36
 * Used for context generation.
37
 */
38
class ContextGenerator
39
{
40
    /**
41
     * Labels and flags that may be used when defining keywords.
42
     *
43
     * @var array<string, int>
44
     */
45
    public static $labelsFlags = [
46
        '(R)' => 2, // reserved
47
        '(D)' => 8, // data type
48
        '(K)' => 16, // keyword
49
        '(F)' => 32, // function name
50
    ];
51
52
    /**
53
     * Documentation links for each context.
54
     *
55
     * @var array<string, string>
56
     */
57
    public static $links = [
58
        'MySql50000' => 'https://dev.mysql.com/doc/refman/5.0/en/keywords.html',
59
        'MySql50100' => 'https://dev.mysql.com/doc/refman/5.1/en/keywords.html',
60
        'MySql50500' => 'https://dev.mysql.com/doc/refman/5.5/en/keywords.html',
61
        'MySql50600' => 'https://dev.mysql.com/doc/refman/5.6/en/keywords.html',
62
        'MySql50700' => 'https://dev.mysql.com/doc/refman/5.7/en/keywords.html',
63
        'MySql80000' => 'https://dev.mysql.com/doc/refman/8.0/en/keywords.html',
64
        'MariaDb100000' => 'https://mariadb.com/kb/en/reserved-words/',
65
        'MariaDb100100' => 'https://mariadb.com/kb/en/reserved-words/',
66
        'MariaDb100200' => 'https://mariadb.com/kb/en/reserved-words/',
67
        'MariaDb100300' => 'https://mariadb.com/kb/en/reserved-words/',
68
        'MariaDb100400' => 'https://mariadb.com/kb/en/reserved-words/',
69
        'MariaDb100500' => 'https://mariadb.com/kb/en/reserved-words/',
70
        'MariaDb100600' => 'https://mariadb.com/kb/en/reserved-words/',
71
    ];
72
73
    /**
74
     * The template of a context.
75
     *
76
     * Parameters:
77
     *     1 - name
78
     *     2 - class
79
     *     3 - link
80
     *     4 - keywords array
81
     */
82
    public const TEMPLATE = <<<'PHP'
83
<?php
84
85
declare(strict_types=1);
86
87
namespace PhpMyAdmin\SqlParser\Contexts;
88
89
use PhpMyAdmin\SqlParser\Context;
90
use PhpMyAdmin\SqlParser\Token;
91
92
/**
93
 * Context for %1$s.
94
 *
95
 * This class was auto-generated from tools/contexts/*.txt.
96
 * Use tools/run_generators.sh for update.
97
 *
98
 * @see %3$s
99
 */
100
class %2$s extends Context
101
{
102
    /**
103
     * List of keywords.
104
     *
105
     * The value associated to each keyword represents its flags.
106
     *
107
     * @see Token::FLAG_KEYWORD_RESERVED Token::FLAG_KEYWORD_COMPOSED
108
     *      Token::FLAG_KEYWORD_DATA_TYPE Token::FLAG_KEYWORD_KEY
109
     *      Token::FLAG_KEYWORD_FUNCTION
110
     *
111
     * @var array<string,int>
112
     * @psalm-var non-empty-array<string,Token::FLAG_KEYWORD_*|int>
113
     * @phpstan-var non-empty-array<non-empty-string,Token::FLAG_KEYWORD_*|int>
114
     */
115
    public static $keywords = [
116
%4$s    ];
117
}
118
119
PHP;
120
121
    /**
122
     * Sorts an array of words.
123
     *
124
     * @param array<int, array<int, array<int, string>>> $arr
125
     *
126
     * @return array<int, array<int, array<int, string>>>
127
     */
128 6
    public static function sortWords(array &$arr)
129
    {
130 6
        ksort($arr);
131 6
        foreach ($arr as &$wordsByLen) {
132 6
            ksort($wordsByLen);
133 6
            foreach ($wordsByLen as &$words) {
134 6
                sort($words, SORT_STRING);
135
            }
136
        }
137
138 6
        return $arr;
139
    }
140
141
    /**
142
     * Reads a list of words and sorts it by type, length and keyword.
143
     *
144
     * @param string[] $files
145
     *
146
     * @return array<int, array<int, array<int, string>>>
147
     */
148 4
    public static function readWords(array $files)
149
    {
150 4
        $words = [];
151 4
        foreach ($files as $file) {
152 4
            $words = array_merge($words, file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
153
        }
154
155
        /** @var array<string, int> $types */
156 4
        $types = [];
157
158 4
        for ($i = 0, $count = count($words); $i !== $count; ++$i) {
159 4
            $type = 1;
160 4
            $value = trim($words[$i]);
161
162
            // Reserved, data types, keys, functions, etc. keywords.
163 4
            foreach (static::$labelsFlags as $label => $flags) {
164 4
                if (strstr($value, $label) === false) {
165 4
                    continue;
166
                }
167
168 4
                $type |= $flags;
169 4
                $value = trim(str_replace($label, '', $value));
170
            }
171
172
            // Composed keyword.
173 4
            if (strstr($value, ' ') !== false) {
174 4
                $type |= 2; // Reserved keyword.
175 4
                $type |= 4; // Composed keyword.
176
            }
177
178 4
            $len = strlen($words[$i]);
179 4
            if ($len === 0) {
180
                continue;
181
            }
182
183 4
            $value = strtoupper($value);
184 4
            if (! isset($types[$value])) {
185 4
                $types[$value] = $type;
186
            } else {
187 4
                $types[$value] |= $type;
188
            }
189
        }
190
191 4
        $ret = [];
192 4
        foreach ($types as $word => $type) {
193 4
            $len = strlen($word);
194 4
            if (! isset($ret[$type])) {
195 4
                $ret[$type] = [];
196
            }
197
198 4
            if (! isset($ret[$type][$len])) {
199 4
                $ret[$type][$len] = [];
200
            }
201
202 4
            $ret[$type][$len][] = $word;
203
        }
204
205 4
        return static::sortWords($ret);
206
    }
207
208
    /**
209
     * Prints an array of a words in PHP format.
210
     *
211
     * @param array<int, array<int, array<int, string>>> $words  the list of words to be formatted
212
     * @param int                                        $spaces the number of spaces that starts every line
213
     * @param int                                        $line   the length of a line
214
     */
215 2
    public static function printWords($words, $spaces = 8, $line = 140): string
216
    {
217 2
        $typesCount = count($words);
218 2
        $ret = '';
219 2
        $j = 0;
220
221 2
        foreach ($words as $type => $wordsByType) {
222 2
            foreach ($wordsByType as $len => $wordsByLen) {
223 2
                $count = round(($line - $spaces) / ($len + 9)); // strlen("'' => 1, ") = 9
224 2
                $i = 0;
225
226 2
                foreach ($wordsByLen as $word) {
227 2
                    if ($i === 0) {
228 2
                        $ret .= str_repeat(' ', $spaces);
229
                    }
230
231 2
                    $ret .= sprintf('\'%s\' => %s, ', $word, $type);
232 2
                    if (++$i !== $count && ++$i <= $count) {
233 2
                        continue;
234
                    }
235
236 2
                    $ret .= "\n";
237 2
                    $i = 0;
238
                }
239
240 2
                if ($i === 0) {
241 2
                    continue;
242
                }
243
244 2
                $ret .= "\n";
245
            }
246
247 2
            if (++$j >= $typesCount) {
248 2
                continue;
249
            }
250
251 2
            $ret .= "\n";
252
        }
253
254
        // Trim trailing spaces and return.
255 2
        return str_replace(" \n", "\n", $ret);
256
    }
257
258
    /**
259
     * Generates a context's class.
260
     *
261
     * @param array<string, string|array<int, array<int, array<int, string>>>> $options the options for this context
262
     * @psalm-param array{
263
     *   name: string,
264
     *   class: string,
265
     *   link: string,
266
     *   keywords: array<int, array<int, array<int, string>>>
267
     * } $options
268
     */
269 2
    public static function generate($options): string
270
    {
271 2
        if (isset($options['keywords'])) {
272 2
            $options['keywords'] = static::printWords($options['keywords']);
0 ignored issues
show
Bug introduced by
It seems like $options['keywords'] can also be of type string; however, parameter $words of PhpMyAdmin\SqlParser\Too...Generator::printWords() does only seem to accept array<integer,array<inte...array<integer,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

272
            $options['keywords'] = static::printWords(/** @scrutinizer ignore-type */ $options['keywords']);
Loading history...
273
        }
274
275 2
        return sprintf(self::TEMPLATE, $options['name'], $options['class'], $options['link'], $options['keywords']);
276
    }
277
278
    /**
279
     * Formats context name.
280
     *
281
     * @param string $name name to format
282
     *
283
     * @return string
284
     */
285 2
    public static function formatName($name)
286
    {
287
        /* Split name and version */
288 2
        $parts = [];
289 2
        if (preg_match('/([^[0-9]*)([0-9]*)/', $name, $parts) === false) {
290
            return $name;
291
        }
292
293
        /* Format name */
294 2
        $base = $parts[1];
295 2
        if ($base === 'MySql') {
296 2
            $base = 'MySQL';
297 2
        } elseif ($base === 'MariaDb') {
298 2
            $base = 'MariaDB';
299
        }
300
301
        /* Parse version to array */
302 2
        $versionString = $parts[2];
303 2
        if (strlen($versionString) % 2 === 1) {
304 2
            $versionString = '0' . $versionString;
305
        }
306
307 2
        $version = array_map('intval', str_split($versionString, 2));
0 ignored issues
show
Bug introduced by
It seems like str_split($versionString, 2) can also be of type true; however, parameter $array of array_map() does only seem to accept array, 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

307
        $version = array_map('intval', /** @scrutinizer ignore-type */ str_split($versionString, 2));
Loading history...
308
        /* Remove trailing zero */
309 2
        if ($version[count($version) - 1] === 0) {
310 2
            $version = array_slice($version, 0, count($version) - 1);
311
        }
312
313
        /* Create name */
314 2
        return $base . ' ' . implode('.', $version);
315
    }
316
317
    /**
318
     * Builds a test.
319
     *
320
     * Reads the input file, generates the data and writes it back.
321
     *
322
     * @param string $input  the input file
323
     * @param string $output the output directory
324
     */
325
    public static function build($input, $output): void
326
    {
327
        /**
328
         * The directory that contains the input file.
329
         *
330
         * Used to include common files.
331
         *
332
         * @var string
333
         */
334
        $directory = dirname($input) . '/';
335
336
        /**
337
         * The name of the file that contains the context.
338
         */
339
        $file = basename($input);
340
341
        /**
342
         * The name of the context.
343
         *
344
         * @var string
345
         */
346
        $name = substr($file, 0, -4);
347
348
        /**
349
         * The name of the class that defines this context.
350
         *
351
         * @var string
352
         */
353
        $class = 'Context' . $name;
354
355
        /**
356
         * The formatted name of this context.
357
         */
358
        $formattedName = static::formatName($name);
359
360
        file_put_contents(
361
            $output . '/' . $class . '.php',
362
            static::generate(
363
                [
0 ignored issues
show
Bug introduced by
array('name' => $formatt..., $directory . $file))) of type array<string,array<integ...teger,string>>>|string> is incompatible with the type array<string,array<integ...teger,string>>>|string> expected by parameter $options of PhpMyAdmin\SqlParser\Too...xtGenerator::generate(). ( Ignorable by Annotation )

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

363
                /** @scrutinizer ignore-type */ [
Loading history...
364
                    'name' => $formattedName,
365
                    'class' => $class,
366
                    'link' => static::$links[$name],
367
                    'keywords' => static::readWords(
368
                        [
369
                            $directory . '_common.txt',
370
                            $directory . '_functions' . $file,
371
                            $directory . $file,
372
                        ]
373
                    ),
374
                ]
375
            )
376
        );
377
    }
378
379
    /**
380
     * Generates recursively all tests preserving the directory structure.
381
     *
382
     * @param string $input  the input directory
383
     * @param string $output the output directory
384
     */
385
    public static function buildAll($input, $output): void
386
    {
387
        $files = scandir($input);
388
389
        foreach ($files as $file) {
390
            // Skipping current and parent directories.
391
            // Skipping _functions* and _common.txt files
392
            if (($file[0] === '.') || ($file[0] === '_')) {
393
                continue;
394
            }
395
396
            // Skipping README.md
397
            if ($file === 'README.md') {
398
                continue;
399
            }
400
401
            // Building the context.
402
            echo sprintf("Building context for %s...\n", $file);
403
            static::build($input . '/' . $file, $output);
404
        }
405
    }
406
}
407