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

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

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

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