Passed
Push — master ( a7a57c...9e99de )
by William
03:11 queued 12s
created

ContextGenerator::formatName()   A

Complexity

Conditions 6
Paths 13

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.0061

Importance

Changes 0
Metric Value
cc 6
eloc 18
c 0
b 0
f 0
nc 13
nop 1
dl 0
loc 33
rs 9.0444
ccs 17
cts 18
cp 0.9444
crap 6.0061
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
     * @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 6
    public static function sortWords(array &$arr)
128
    {
129 6
        ksort($arr);
130 6
        foreach ($arr as &$wordsByLen) {
131 6
            ksort($wordsByLen);
132 6
            foreach ($wordsByLen as &$words) {
133 6
                sort($words, SORT_STRING);
134
            }
135
        }
136
137 6
        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 4
    public static function readWords(array $files)
148
    {
149 4
        $words = [];
150 4
        foreach ($files as $file) {
151 4
            $words = array_merge($words, file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
152
        }
153
154
        /** @var array<string, int> $types */
155 4
        $types = [];
156
157 4
        for ($i = 0, $count = count($words); $i !== $count; ++$i) {
158 4
            $type = 1;
159 4
            $value = trim($words[$i]);
160
161
            // Reserved, data types, keys, functions, etc. keywords.
162 4
            foreach (static::$labelsFlags as $label => $flags) {
163 4
                if (strstr($value, $label) === false) {
164 4
                    continue;
165
                }
166
167 4
                $type |= $flags;
168 4
                $value = trim(str_replace($label, '', $value));
169
            }
170
171
            // Composed keyword.
172 4
            if (strstr($value, ' ') !== false) {
173 4
                $type |= 2; // Reserved keyword.
174 4
                $type |= 4; // Composed keyword.
175
            }
176
177 4
            $len = strlen($words[$i]);
178 4
            if ($len === 0) {
179
                continue;
180
            }
181
182 4
            $value = strtoupper($value);
183 4
            if (! isset($types[$value])) {
184 4
                $types[$value] = $type;
185
            } else {
186 4
                $types[$value] |= $type;
187
            }
188
        }
189
190 4
        $ret = [];
191 4
        foreach ($types as $word => $type) {
192 4
            $len = strlen($word);
193 4
            if (! isset($ret[$type])) {
194 4
                $ret[$type] = [];
195
            }
196
197 4
            if (! isset($ret[$type][$len])) {
198 4
                $ret[$type][$len] = [];
199
            }
200
201 4
            $ret[$type][$len][] = $word;
202
        }
203
204 4
        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 2
    public static function printWords($words, $spaces = 8, $line = 140)
217
    {
218 2
        $typesCount = count($words);
219 2
        $ret = '';
220 2
        $j = 0;
221
222 2
        foreach ($words as $type => $wordsByType) {
223 2
            foreach ($wordsByType as $len => $wordsByLen) {
224 2
                $count = round(($line - $spaces) / ($len + 9)); // strlen("'' => 1, ") = 9
225 2
                $i = 0;
226
227 2
                foreach ($wordsByLen as $word) {
228 2
                    if ($i === 0) {
229 2
                        $ret .= str_repeat(' ', $spaces);
230
                    }
231
232 2
                    $ret .= sprintf('\'%s\' => %s, ', $word, $type);
233 2
                    if (++$i !== $count && ++$i <= $count) {
234 2
                        continue;
235
                    }
236
237 2
                    $ret .= "\n";
238 2
                    $i = 0;
239
                }
240
241 2
                if ($i === 0) {
242 2
                    continue;
243
                }
244
245 2
                $ret .= "\n";
246
            }
247
248 2
            if (++$j >= $typesCount) {
249 2
                continue;
250
            }
251
252 2
            $ret .= "\n";
253
        }
254
255
        // Trim trailing spaces and return.
256 2
        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 2
    public static function generate($options)
273
    {
274 2
        if (isset($options['keywords'])) {
275 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

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

313
        $version = array_map('intval', /** @scrutinizer ignore-type */ str_split($versionString, 2));
Loading history...
314
        /* Remove trailing zero */
315 2
        if ($version[count($version) - 1] === 0) {
316 2
            $version = array_slice($version, 0, count($version) - 1);
317
        }
318
319
        /* Create name */
320 2
        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
            // Skipping _functions* and _common.txt files
402
            if (($file[0] === '.') || ($file[0] === '_')) {
403
                continue;
404
            }
405
406
            // Skipping README.md
407
            if ($file === 'README.md') {
408
                continue;
409
            }
410
411
            // Building the context.
412
            echo sprintf("Building context for %s...\n", $file);
413
            static::build($input . '/' . $file, $output);
414
        }
415
    }
416
}
417