Passed
Pull Request — master (#462)
by
unknown
03:19
created

ContextGenerator::readWords()   C

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 0
Metric Value
cc 11
eloc 32
c 0
b 0
f 0
nc 190
nop 1
dl 0
loc 58
rs 6.5666
ccs 31
cts 32
cp 0.9688
crap 11.0036

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_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
     * @return void
325
     */
326
    public static function build($input, $output)
327
    {
328
        /**
329
         * The directory that contains the input file.
330
         *
331
         * Used to include common files.
332
         *
333
         * @var string
334
         */
335
        $directory = dirname($input) . '/';
336
337
        /**
338
         * The name of the file that contains the context.
339
         */
340
        $file = basename($input);
341
342
        /**
343
         * The name of the context.
344
         *
345
         * @var string
346
         */
347
        $name = substr($file, 0, -4);
348
349
        /**
350
         * The name of the class that defines this context.
351
         *
352
         * @var string
353
         */
354
        $class = 'Context' . $name;
355
356
        /**
357
         * The formatted name of this context.
358
         */
359
        $formattedName = static::formatName($name);
360
361
        file_put_contents(
362
            $output . '/' . $class . '.php',
363
            static::generate(
364
                [
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

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