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
![]() |
|||||
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
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
![]() |
|||||
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 |