Passed
Push — master ( d42b40...87af0c )
by Maurício
06:31 queued 03:28
created

ContextGenerator::generate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
ccs 4
cts 4
cp 1
crap 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_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 2
    public static function printWords($words, $spaces = 8, $line = 140): string
215
    {
216 2
        $typesCount = count($words);
217 2
        $ret = '';
218 2
        $j = 0;
219
220 2
        foreach ($words as $type => $wordsByType) {
221 2
            foreach ($wordsByType as $len => $wordsByLen) {
222 2
                $count = round(($line - $spaces) / ($len + 9)); // strlen("'' => 1, ") = 9
223 2
                $i = 0;
224
225 2
                foreach ($wordsByLen as $word) {
226 2
                    if ($i === 0) {
227 2
                        $ret .= str_repeat(' ', $spaces);
228
                    }
229
230 2
                    $ret .= sprintf('\'%s\' => %s, ', $word, $type);
231 2
                    if (++$i !== $count && ++$i <= $count) {
232 2
                        continue;
233
                    }
234
235 2
                    $ret .= "\n";
236 2
                    $i = 0;
237
                }
238
239 2
                if ($i === 0) {
240 2
                    continue;
241
                }
242
243 2
                $ret .= "\n";
244
            }
245
246 2
            if (++$j >= $typesCount) {
247 2
                continue;
248
            }
249
250 2
            $ret .= "\n";
251
        }
252
253
        // Trim trailing spaces and return.
254 2
        return str_replace(" \n", "\n", $ret);
255
    }
256
257
    /**
258
     * Generates a context's class.
259
     *
260
     * @param array<string, string|array<int, array<int, array<int, string>>>> $options the options for this context
261
     * @psalm-param array{
262
     *   name: string,
263
     *   class: string,
264
     *   link: string,
265
     *   keywords: array<int, array<int, array<int, string>>>
266
     * } $options
267
     */
268 2
    public static function generate($options): string
269
    {
270 2
        if (isset($options['keywords'])) {
271 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

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

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

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