ContextGenerator::readWords()   C
last analyzed

Complexity

Conditions 11
Paths 190

Size

Total Lines 58
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 11.0036

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 32
c 1
b 0
f 0
nc 190
nop 1
dl 0
loc 58
ccs 31
cts 32
cp 0.9688
crap 11.0036
rs 6.5666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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