ContextGenerator   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Test Coverage

Coverage 70.18%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 171
c 4
b 0
f 0
dl 0
loc 371
rs 9.44
ccs 80
cts 114
cp 0.7018
wmc 37

7 Methods

Rating   Name   Duplication   Size   Complexity  
B printWords() 0 41 9
A formatName() 0 30 6
A buildAll() 0 19 5
C readWords() 0 58 11
A build() 0 41 1
A sortWords() 0 11 3
A generate() 0 7 2
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_contains;
23
use function str_repeat;
24
use function str_replace;
25
use function str_split;
26
use function strlen;
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 array $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 array $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
        'MySql80100' => 'https://dev.mysql.com/doc/refman/8.1/en/keywords.html',
65
        'MySql80200' => 'https://dev.mysql.com/doc/refman/8.2/en/keywords.html',
66
        'MySql80300' => 'https://dev.mysql.com/doc/refman/8.3/en/keywords.html',
67
        'MariaDb100000' => 'https://mariadb.com/kb/en/reserved-words/',
68
        'MariaDb100100' => 'https://mariadb.com/kb/en/reserved-words/',
69
        'MariaDb100200' => 'https://mariadb.com/kb/en/reserved-words/',
70
        'MariaDb100300' => 'https://mariadb.com/kb/en/reserved-words/',
71
        'MariaDb100400' => 'https://mariadb.com/kb/en/reserved-words/',
72
        'MariaDb100500' => 'https://mariadb.com/kb/en/reserved-words/',
73
        'MariaDb100600' => 'https://mariadb.com/kb/en/reserved-words/',
74
        'MariaDb100700' => 'https://mariadb.com/kb/en/reserved-words/',
75
        'MariaDb100800' => 'https://mariadb.com/kb/en/reserved-words/',
76
        'MariaDb100900' => 'https://mariadb.com/kb/en/reserved-words/',
77
        'MariaDb101000' => 'https://mariadb.com/kb/en/reserved-words/',
78
        'MariaDb101100' => 'https://mariadb.com/kb/en/reserved-words/',
79
        'MariaDb110000' => 'https://mariadb.com/kb/en/reserved-words/',
80
        'MariaDb110100' => 'https://mariadb.com/kb/en/reserved-words/',
81
        'MariaDb110200' => 'https://mariadb.com/kb/en/reserved-words/',
82
        'MariaDb110300' => 'https://mariadb.com/kb/en/reserved-words/',
83
        'MariaDb110400' => 'https://mariadb.com/kb/en/reserved-words/',
84
    ];
85
86
    /**
87
     * The template of a context.
88
     *
89
     * Parameters:
90
     *     1 - name
91
     *     2 - class
92
     *     3 - link
93
     *     4 - keywords array
94
     */
95
    public const TEMPLATE = <<<'PHP'
96
<?php
97
98
declare(strict_types=1);
99
100
namespace PhpMyAdmin\SqlParser\Contexts;
101
102
use PhpMyAdmin\SqlParser\Context;
103
use PhpMyAdmin\SqlParser\Token;
104
105
/**
106
 * Context for %1$s.
107
 *
108
 * This class was auto-generated from tools/contexts/*.txt.
109
 * Use tools/run_generators.sh for update.
110
 *
111
 * @see %3$s
112
 */
113
class %2$s extends Context
114
{
115
    /**
116
     * List of keywords.
117
     *
118
     * The value associated to each keyword represents its flags.
119
     *
120
     * @see Token::FLAG_KEYWORD_RESERVED Token::FLAG_KEYWORD_COMPOSED
121
     *      Token::FLAG_KEYWORD_DATA_TYPE Token::FLAG_KEYWORD_KEY
122
     *      Token::FLAG_KEYWORD_FUNCTION
123
     *
124
     * @var array<string,int>
125
     * @psalm-var non-empty-array<string,Token::FLAG_KEYWORD_*|int>
126
     * @phpstan-var non-empty-array<non-empty-string,Token::FLAG_KEYWORD_*|int>
127
     */
128
    public static array $keywords = [
129
%4$s    ];
130
}
131
132
PHP;
133
134
    /**
135
     * Sorts an array of words.
136
     *
137
     * @param array<int, array<int, array<int, string>>> $arr
138
     *
139
     * @return array<int, array<int, array<int, string>>>
140
     */
141 6
    public static function sortWords(array &$arr): array
142
    {
143 6
        ksort($arr);
144 6
        foreach ($arr as &$wordsByLen) {
145 6
            ksort($wordsByLen);
146 6
            foreach ($wordsByLen as &$words) {
147 6
                sort($words, SORT_STRING);
148
            }
149
        }
150
151 6
        return $arr;
152
    }
153
154
    /**
155
     * Reads a list of words and sorts it by type, length and keyword.
156
     *
157
     * @param string[] $files
158
     *
159
     * @return array<int, array<int, array<int, string>>>
160
     */
161 4
    public static function readWords(array $files): array
162
    {
163 4
        $words = [];
164 4
        foreach ($files as $file) {
165 4
            $words = array_merge($words, file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
166
        }
167
168
        /** @var array<string, int> $types */
169 4
        $types = [];
170
171 4
        for ($i = 0, $count = count($words); $i !== $count; ++$i) {
172 4
            $type = 1;
173 4
            $value = trim($words[$i]);
174
175
            // Reserved, data types, keys, functions, etc. keywords.
176 4
            foreach (static::$labelsFlags as $label => $flags) {
177 4
                if (! str_contains($value, $label)) {
178 4
                    continue;
179
                }
180
181 4
                $type |= $flags;
182 4
                $value = trim(str_replace($label, '', $value));
183
            }
184
185
            // Composed keyword.
186 4
            if (str_contains($value, ' ')) {
187 4
                $type |= 2; // Reserved keyword.
188 4
                $type |= 4; // Composed keyword.
189
            }
190
191 4
            $len = strlen($words[$i]);
192 4
            if ($len === 0) {
193
                continue;
194
            }
195
196 4
            $value = strtoupper($value);
197 4
            if (! isset($types[$value])) {
198 4
                $types[$value] = $type;
199
            } else {
200 4
                $types[$value] |= $type;
201
            }
202
        }
203
204 4
        $ret = [];
205 4
        foreach ($types as $word => $type) {
206 4
            $len = strlen($word);
207 4
            if (! isset($ret[$type])) {
208 4
                $ret[$type] = [];
209
            }
210
211 4
            if (! isset($ret[$type][$len])) {
212 4
                $ret[$type][$len] = [];
213
            }
214
215 4
            $ret[$type][$len][] = $word;
216
        }
217
218 4
        return static::sortWords($ret);
219
    }
220
221
    /**
222
     * Prints an array of a words in PHP format.
223
     *
224
     * @param array<int, array<int, array<int, string>>> $words  the list of words to be formatted
225
     * @param int                                        $spaces the number of spaces that starts every line
226
     * @param int                                        $line   the length of a line
227
     */
228 2
    public static function printWords(array $words, int $spaces = 8, int $line = 140): string
229
    {
230 2
        $typesCount = count($words);
231 2
        $ret = '';
232 2
        $j = 0;
233
234 2
        foreach ($words as $type => $wordsByType) {
235 2
            foreach ($wordsByType as $len => $wordsByLen) {
236 2
                $count = round(($line - $spaces) / ($len + 9)); // strlen("'' => 1, ") = 9
237 2
                $i = 0;
238
239 2
                foreach ($wordsByLen as $word) {
240 2
                    if ($i === 0) {
241 2
                        $ret .= str_repeat(' ', $spaces);
242
                    }
243
244 2
                    $ret .= sprintf('\'%s\' => %s, ', $word, $type);
245 2
                    if (++$i !== $count && ++$i <= $count) {
246 2
                        continue;
247
                    }
248
249 2
                    $ret .= "\n";
250 2
                    $i = 0;
251
                }
252
253 2
                if ($i === 0) {
254 2
                    continue;
255
                }
256
257 2
                $ret .= "\n";
258
            }
259
260 2
            if (++$j >= $typesCount) {
261 2
                continue;
262
            }
263
264 2
            $ret .= "\n";
265
        }
266
267
        // Trim trailing spaces and return.
268 2
        return str_replace(" \n", "\n", $ret);
269
    }
270
271
    /**
272
     * Generates a context's class.
273
     *
274
     * @param array<string, string|array<int, array<int, array<int, string>>>> $options the options for this context
275
     * @psalm-param array{
276
     *   name: string,
277
     *   class: string,
278
     *   link: string,
279
     *   keywords: array<int, array<int, array<int, string>>>
280
     * } $options
281
     */
282 2
    public static function generate(array $options): string
283
    {
284 2
        if (isset($options['keywords'])) {
285 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, 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

285
            $options['keywords'] = static::printWords(/** @scrutinizer ignore-type */ $options['keywords']);
Loading history...
286
        }
287
288 2
        return sprintf(self::TEMPLATE, $options['name'], $options['class'], $options['link'], $options['keywords']);
289
    }
290
291
    /**
292
     * Formats context name.
293
     *
294
     * @param string $name name to format
295
     */
296 2
    public static function formatName(string $name): string
297
    {
298
        /* Split name and version */
299 2
        $parts = [];
300 2
        if (preg_match('/([^[0-9]*)([0-9]*)/', $name, $parts) === false) {
301
            return $name;
302
        }
303
304
        /* Format name */
305 2
        $base = $parts[1];
306 2
        if ($base === 'MySql') {
307 2
            $base = 'MySQL';
308 2
        } elseif ($base === 'MariaDb') {
309 2
            $base = 'MariaDB';
310
        }
311
312
        /* Parse version to array */
313 2
        $versionString = $parts[2];
314 2
        if (strlen($versionString) % 2 === 1) {
315 2
            $versionString = '0' . $versionString;
316
        }
317
318 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

318
        $version = array_map('intval', /** @scrutinizer ignore-type */ str_split($versionString, 2));
Loading history...
319
        /* Remove trailing zero */
320 2
        if ($version[count($version) - 1] === 0) {
321 2
            $version = array_slice($version, 0, count($version) - 1);
322
        }
323
324
        /* Create name */
325 2
        return $base . ' ' . implode('.', $version);
326
    }
327
328
    /**
329
     * Builds a test.
330
     *
331
     * Reads the input file, generates the data and writes it back.
332
     *
333
     * @param string $input  the input file
334
     * @param string $output the output directory
335
     */
336
    public static function build(string $input, string $output): void
337
    {
338
        /**
339
         * The directory that contains the input file.
340
         *
341
         * Used to include common files.
342
         */
343
        $directory = dirname($input) . '/';
344
345
        /**
346
         * The name of the file that contains the context.
347
         */
348
        $file = basename($input);
349
350
        /**
351
         * The name of the context.
352
         */
353
        $name = substr($file, 0, -4);
354
355
        /**
356
         * The name of the class that defines this context.
357
         */
358
        $class = 'Context' . $name;
359
360
        /**
361
         * The formatted name of this context.
362
         */
363
        $formattedName = static::formatName($name);
364
365
        file_put_contents(
366
            $output . '/' . $class . '.php',
367
            static::generate(
368
                [
369
                    'name' => $formattedName,
370
                    'class' => $class,
371
                    'link' => static::$links[$name],
372
                    'keywords' => static::readWords(
373
                        [
374
                            $directory . '_common.txt',
375
                            $directory . '_functions' . $file,
376
                            $directory . $file,
377
                        ],
378
                    ),
379
                ],
380
            ),
381
        );
382
    }
383
384
    /**
385
     * Generates recursively all tests preserving the directory structure.
386
     *
387
     * @param string $input  the input directory
388
     * @param string $output the output directory
389
     */
390
    public static function buildAll(string $input, string $output): void
391
    {
392
        $files = scandir($input);
393
394
        foreach ($files as $file) {
395
            // Skipping current and parent directories.
396
            // Skipping _functions* and _common.txt files
397
            if (($file[0] === '.') || ($file[0] === '_')) {
398
                continue;
399
            }
400
401
            // Skipping README.md
402
            if ($file === 'README.md') {
403
                continue;
404
            }
405
406
            // Building the context.
407
            echo sprintf("Building context for %s...\n", $file);
408
            static::build($input . '/' . $file, $output);
409
        }
410
    }
411
}
412