Passed
Push — master ( 506d4a...b86a80 )
by William
03:22
created

ContextGenerator   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Test Coverage

Coverage 71.93%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 158
c 5
b 0
f 0
dl 0
loc 370
ccs 82
cts 114
cp 0.7193
rs 9.52
wmc 36

7 Methods

Rating   Name   Duplication   Size   Complexity  
A buildAll() 0 13 4
A sortWords() 0 11 3
A generate() 0 7 2
A formatName() 0 33 6
B printWords() 0 41 9
C readWords() 0 58 11
A build() 0 47 1
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 $LABELS_FLAGS = [
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
     * @phpstan-var non-empty-array<non-empty-string,Token::FLAG_KEYWORD_*|int>
113
     */
114
    public static $KEYWORDS = [
115
%4$s    ];
116
}
117
118
PHP;
119
120
    /**
121
     * Sorts an array of words.
122
     *
123
     * @param array<int, array<int, array<int, string>>> $arr
124
     *
125
     * @return array<int, array<int, array<int, string>>>
126
     */
127 12
    public static function sortWords(array &$arr)
128
    {
129 12
        ksort($arr);
130 12
        foreach ($arr as &$wordsByLen) {
131 12
            ksort($wordsByLen);
132 12
            foreach ($wordsByLen as &$words) {
133 12
                sort($words, SORT_STRING);
134
            }
135
        }
136
137 12
        return $arr;
138
    }
139
140
    /**
141
     * Reads a list of words and sorts it by type, length and keyword.
142
     *
143
     * @param string[] $files
144
     *
145
     * @return array<int, array<int, array<int, string>>>
146
     */
147 8
    public static function readWords(array $files)
148
    {
149 8
        $words = [];
150 8
        foreach ($files as $file) {
151 8
            $words = array_merge($words, file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
152
        }
153
154
        /** @var array<string, int> $types */
155 8
        $types = [];
156
157 8
        for ($i = 0, $count = count($words); $i !== $count; ++$i) {
158 8
            $type = 1;
159 8
            $value = trim($words[$i]);
160
161
            // Reserved, data types, keys, functions, etc. keywords.
162 8
            foreach (static::$LABELS_FLAGS as $label => $flags) {
163 8
                if (strstr($value, $label) === false) {
164 8
                    continue;
165
                }
166
167 8
                $type |= $flags;
168 8
                $value = trim(str_replace($label, '', $value));
169
            }
170
171
            // Composed keyword.
172 8
            if (strstr($value, ' ') !== false) {
173 8
                $type |= 2; // Reserved keyword.
174 8
                $type |= 4; // Composed keyword.
175
            }
176
177 8
            $len = strlen($words[$i]);
178 8
            if ($len === 0) {
179
                continue;
180
            }
181
182 8
            $value = strtoupper($value);
183 8
            if (! isset($types[$value])) {
184 8
                $types[$value] = $type;
185
            } else {
186 8
                $types[$value] |= $type;
187
            }
188
        }
189
190 8
        $ret = [];
191 8
        foreach ($types as $word => $type) {
192 8
            $len = strlen($word);
193 8
            if (! isset($ret[$type])) {
194 8
                $ret[$type] = [];
195
            }
196
197 8
            if (! isset($ret[$type][$len])) {
198 8
                $ret[$type][$len] = [];
199
            }
200
201 8
            $ret[$type][$len][] = $word;
202
        }
203
204 8
        return static::sortWords($ret);
205
    }
206
207
    /**
208
     * Prints an array of a words in PHP format.
209
     *
210
     * @param array<int, array<int, array<int, string>>> $words  the list of words to be formatted
211
     * @param int                                        $spaces the number of spaces that starts every line
212
     * @param int                                        $line   the length of a line
213
     *
214
     * @return string
215
     */
216 4
    public static function printWords($words, $spaces = 8, $line = 140)
217
    {
218 4
        $typesCount = count($words);
219 4
        $ret = '';
220 4
        $j = 0;
221
222 4
        foreach ($words as $type => $wordsByType) {
223 4
            foreach ($wordsByType as $len => $wordsByLen) {
224 4
                $count = round(($line - $spaces) / ($len + 9)); // strlen("'' => 1, ") = 9
225 4
                $i = 0;
226
227 4
                foreach ($wordsByLen as $word) {
228 4
                    if ($i === 0) {
229 4
                        $ret .= str_repeat(' ', $spaces);
230
                    }
231
232 4
                    $ret .= sprintf('\'%s\' => %s, ', $word, $type);
233 4
                    if (++$i !== $count && ++$i <= $count) {
234 4
                        continue;
235
                    }
236
237 4
                    $ret .= "\n";
238 4
                    $i = 0;
239
                }
240
241 4
                if ($i === 0) {
242 4
                    continue;
243
                }
244
245 4
                $ret .= "\n";
246
            }
247
248 4
            if (++$j >= $typesCount) {
249 4
                continue;
250
            }
251
252 4
            $ret .= "\n";
253
        }
254
255
        // Trim trailing spaces and return.
256 4
        return str_replace(" \n", "\n", $ret);
257
    }
258
259
    /**
260
     * Generates a context's class.
261
     *
262
     * @param array<string, string|array<int, array<int, array<int, string>>>> $options the options for this context
263
     * @psalm-param array{
264
     *   name: string,
265
     *   class: string,
266
     *   link: string,
267
     *   keywords: array<int, array<int, array<int, string>>>
268
     * } $options
269
     *
270
     * @return string
271
     */
272 4
    public static function generate($options)
273
    {
274 4
        if (isset($options['keywords'])) {
275 4
            $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

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

313
        $version = array_map('intval', /** @scrutinizer ignore-type */ str_split($versionString, 2));
Loading history...
314
        /* Remove trailing zero */
315 4
        if ($version[count($version) - 1] === 0) {
316 4
            $version = array_slice($version, 0, count($version) - 1);
317
        }
318
319
        /* Create name */
320 4
        return $base . ' ' . implode('.', $version);
321
    }
322
323
    /**
324
     * Builds a test.
325
     *
326
     * Reads the input file, generates the data and writes it back.
327
     *
328
     * @param string $input  the input file
329
     * @param string $output the output directory
330
     *
331
     * @return void
332
     */
333
    public static function build($input, $output)
334
    {
335
        /**
336
         * The directory that contains the input file.
337
         *
338
         * Used to include common files.
339
         *
340
         * @var string
341
         */
342
        $directory = dirname($input) . '/';
343
344
        /**
345
         * The name of the file that contains the context.
346
         */
347
        $file = basename($input);
348
349
        /**
350
         * The name of the context.
351
         *
352
         * @var string
353
         */
354
        $name = substr($file, 0, -4);
355
356
        /**
357
         * The name of the class that defines this context.
358
         *
359
         * @var string
360
         */
361
        $class = 'Context' . $name;
362
363
        /**
364
         * The formatted name of this context.
365
         */
366
        $formattedName = static::formatName($name);
367
368
        file_put_contents(
369
            $output . '/' . $class . '.php',
370
            static::generate(
371
                [
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

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