phpmyadmin /
sql-parser
| 1 | <?php |
||||
| 2 | |||||
| 3 | declare(strict_types=1); |
||||
| 4 | |||||
| 5 | namespace PhpMyAdmin\SqlParser\Utils; |
||||
| 6 | |||||
| 7 | use PhpMyAdmin\SqlParser\Components\JoinKeyword; |
||||
| 8 | use PhpMyAdmin\SqlParser\Lexer; |
||||
| 9 | use PhpMyAdmin\SqlParser\Parser; |
||||
| 10 | use PhpMyAdmin\SqlParser\Token; |
||||
| 11 | use PhpMyAdmin\SqlParser\TokensList; |
||||
| 12 | use PhpMyAdmin\SqlParser\TokenType; |
||||
| 13 | |||||
| 14 | use function array_merge; |
||||
| 15 | use function array_pop; |
||||
| 16 | use function end; |
||||
| 17 | use function htmlspecialchars; |
||||
| 18 | use function in_array; |
||||
| 19 | use function mb_strlen; |
||||
| 20 | use function str_contains; |
||||
| 21 | use function str_repeat; |
||||
| 22 | use function str_replace; |
||||
| 23 | use function strtoupper; |
||||
| 24 | |||||
| 25 | use const ENT_NOQUOTES; |
||||
| 26 | use const PHP_SAPI; |
||||
| 27 | |||||
| 28 | /** |
||||
| 29 | * Utilities that are used for formatting queries. |
||||
| 30 | */ |
||||
| 31 | class Formatter |
||||
| 32 | { |
||||
| 33 | /** |
||||
| 34 | * The formatting options. |
||||
| 35 | * |
||||
| 36 | * @var array<string, bool|string|array<int, array<string, int|string>>> |
||||
| 37 | */ |
||||
| 38 | public array $options; |
||||
| 39 | |||||
| 40 | /** |
||||
| 41 | * Clauses that are usually short. |
||||
| 42 | * |
||||
| 43 | * These clauses share the line with the next clause. |
||||
| 44 | * |
||||
| 45 | * E.g. if INSERT was not here, the formatter would produce: |
||||
| 46 | * |
||||
| 47 | * INSERT |
||||
| 48 | * INTO foo |
||||
| 49 | * VALUES(0, 0, 0),(1, 1, 1); |
||||
| 50 | * |
||||
| 51 | * Instead of: |
||||
| 52 | * |
||||
| 53 | * INSERT INTO foo |
||||
| 54 | * VALUES(0, 0, 0),(1, 1, 1) |
||||
| 55 | * |
||||
| 56 | * @var array<string, bool> |
||||
| 57 | */ |
||||
| 58 | public static array $shortClauses = [ |
||||
| 59 | 'CREATE' => true, |
||||
| 60 | 'INSERT' => true, |
||||
| 61 | ]; |
||||
| 62 | |||||
| 63 | /** |
||||
| 64 | * Clauses that must be inlined. |
||||
| 65 | * |
||||
| 66 | * These clauses usually are short and it's nicer to have them inline. |
||||
| 67 | * |
||||
| 68 | * @var array<string, bool> |
||||
| 69 | */ |
||||
| 70 | public static array $inlineClauses = [ |
||||
| 71 | 'CREATE' => true, |
||||
| 72 | 'INTO' => true, |
||||
| 73 | 'LIMIT' => true, |
||||
| 74 | 'PARTITION BY' => true, |
||||
| 75 | 'PARTITION' => true, |
||||
| 76 | 'PROCEDURE' => true, |
||||
| 77 | 'SUBPARTITION BY' => true, |
||||
| 78 | 'VALUES' => true, |
||||
| 79 | ]; |
||||
| 80 | |||||
| 81 | private const FORMATTERS = [ |
||||
| 82 | 'PARTITION BY', |
||||
| 83 | 'SUBPARTITION BY', |
||||
| 84 | ]; |
||||
| 85 | |||||
| 86 | /** @param array<string, bool|string|array<int, array<string, int|string>>> $options the formatting options */ |
||||
| 87 | 48 | public function __construct(array $options = []) |
|||
| 88 | { |
||||
| 89 | 48 | $this->options = $this->getMergedOptions($options); |
|||
| 90 | } |
||||
| 91 | |||||
| 92 | /** |
||||
| 93 | * The specified formatting options are merged with the default values. |
||||
| 94 | * |
||||
| 95 | * @param array<string, bool|string|array<int, array<string, int|string>>> $options |
||||
| 96 | * |
||||
| 97 | * @return array<string, bool|string|array<int, array<string, int|string>>> |
||||
| 98 | */ |
||||
| 99 | 56 | protected function getMergedOptions(array $options): array |
|||
| 100 | { |
||||
| 101 | 56 | $options = array_merge( |
|||
| 102 | 56 | $this->getDefaultOptions(), |
|||
| 103 | 56 | $options, |
|||
| 104 | 56 | ); |
|||
| 105 | |||||
| 106 | 56 | if (isset($options['formats'])) { |
|||
| 107 | 8 | $options['formats'] = self::mergeFormats($this->getDefaultFormats(), $options['formats']); |
|||
| 108 | } else { |
||||
| 109 | 48 | $options['formats'] = $this->getDefaultFormats(); |
|||
| 110 | } |
||||
| 111 | |||||
| 112 | 56 | if ($options['line_ending'] === null) { |
|||
| 113 | 48 | $options['line_ending'] = $options['type'] === 'html' ? '<br/>' : "\n"; |
|||
| 114 | } |
||||
| 115 | |||||
| 116 | 56 | if ($options['indentation'] === null) { |
|||
| 117 | 56 | $options['indentation'] = $options['type'] === 'html' ? ' ' : ' '; |
|||
| 118 | } |
||||
| 119 | |||||
| 120 | // `parts_newline` requires `clause_newline` |
||||
| 121 | 56 | $options['parts_newline'] &= $options['clause_newline']; |
|||
| 122 | |||||
| 123 | 56 | return $options; |
|||
| 124 | } |
||||
| 125 | |||||
| 126 | /** |
||||
| 127 | * The default formatting options. |
||||
| 128 | * |
||||
| 129 | * @return array<string, bool|string|null> |
||||
| 130 | * @psalm-return array{ |
||||
| 131 | * type: ('cli'|'text'), |
||||
| 132 | * line_ending: null, |
||||
| 133 | * indentation: null, |
||||
| 134 | * remove_comments: false, |
||||
| 135 | * clause_newline: true, |
||||
| 136 | * parts_newline: true, |
||||
| 137 | * indent_parts: true |
||||
| 138 | * } |
||||
| 139 | */ |
||||
| 140 | 48 | protected function getDefaultOptions(): array |
|||
| 141 | { |
||||
| 142 | 48 | return [ |
|||
| 143 | /* |
||||
| 144 | * The format of the result. |
||||
| 145 | * |
||||
| 146 | * @var string The type ('text', 'cli' or 'html') |
||||
| 147 | */ |
||||
| 148 | 48 | 'type' => PHP_SAPI === 'cli' ? 'cli' : 'text', |
|||
| 149 | |||||
| 150 | /* |
||||
| 151 | * The line ending used. |
||||
| 152 | * By default, for text this is "\n" and for HTML this is "<br/>". |
||||
| 153 | * |
||||
| 154 | * @var string |
||||
| 155 | */ |
||||
| 156 | 48 | 'line_ending' => null, |
|||
| 157 | |||||
| 158 | /* |
||||
| 159 | * The string used for indentation. |
||||
| 160 | * |
||||
| 161 | * @var string |
||||
| 162 | */ |
||||
| 163 | 48 | 'indentation' => null, |
|||
| 164 | |||||
| 165 | /* |
||||
| 166 | * Whether comments should be removed or not. |
||||
| 167 | * |
||||
| 168 | * @var bool |
||||
| 169 | */ |
||||
| 170 | 48 | 'remove_comments' => false, |
|||
| 171 | |||||
| 172 | /* |
||||
| 173 | * Whether each clause should be on a new line. |
||||
| 174 | * |
||||
| 175 | * @var bool |
||||
| 176 | */ |
||||
| 177 | 48 | 'clause_newline' => true, |
|||
| 178 | |||||
| 179 | /* |
||||
| 180 | * Whether each part should be on a new line. |
||||
| 181 | * Parts are delimited by brackets and commas. |
||||
| 182 | * |
||||
| 183 | * @var bool |
||||
| 184 | */ |
||||
| 185 | 48 | 'parts_newline' => true, |
|||
| 186 | |||||
| 187 | /* |
||||
| 188 | * Whether each part of each clause should be indented. |
||||
| 189 | * |
||||
| 190 | * @var bool |
||||
| 191 | */ |
||||
| 192 | 48 | 'indent_parts' => true, |
|||
| 193 | 48 | ]; |
|||
| 194 | } |
||||
| 195 | |||||
| 196 | /** |
||||
| 197 | * The styles used for HTML formatting. |
||||
| 198 | * [$type, $flags, $span, $callback]. |
||||
| 199 | * |
||||
| 200 | * @return array<int, array<string, int|string>> |
||||
| 201 | * @psalm-return list<array{type: int, flags: int, html: string, cli: string, function: string}> |
||||
| 202 | */ |
||||
| 203 | 48 | protected function getDefaultFormats(): array |
|||
| 204 | { |
||||
| 205 | 48 | return [ |
|||
| 206 | 48 | [ |
|||
| 207 | 48 | 'type' => TokenType::Keyword->value, |
|||
| 208 | 48 | 'flags' => Token::FLAG_KEYWORD_RESERVED, |
|||
| 209 | 48 | 'html' => 'class="sql-reserved"', |
|||
| 210 | 48 | 'cli' => "\x1b[35m", |
|||
| 211 | 48 | 'function' => 'strtoupper', |
|||
| 212 | 48 | ], |
|||
| 213 | 48 | [ |
|||
| 214 | 48 | 'type' => TokenType::Keyword->value, |
|||
| 215 | 48 | 'flags' => 0, |
|||
| 216 | 48 | 'html' => 'class="sql-keyword"', |
|||
| 217 | 48 | 'cli' => "\x1b[95m", |
|||
| 218 | 48 | 'function' => 'strtoupper', |
|||
| 219 | 48 | ], |
|||
| 220 | 48 | [ |
|||
| 221 | 48 | 'type' => TokenType::Comment->value, |
|||
| 222 | 48 | 'flags' => 0, |
|||
| 223 | 48 | 'html' => 'class="sql-comment"', |
|||
| 224 | 48 | 'cli' => "\x1b[37m", |
|||
| 225 | 48 | 'function' => '', |
|||
| 226 | 48 | ], |
|||
| 227 | 48 | [ |
|||
| 228 | 48 | 'type' => TokenType::Bool->value, |
|||
| 229 | 48 | 'flags' => 0, |
|||
| 230 | 48 | 'html' => 'class="sql-atom"', |
|||
| 231 | 48 | 'cli' => "\x1b[36m", |
|||
| 232 | 48 | 'function' => 'strtoupper', |
|||
| 233 | 48 | ], |
|||
| 234 | 48 | [ |
|||
| 235 | 48 | 'type' => TokenType::Number->value, |
|||
| 236 | 48 | 'flags' => 0, |
|||
| 237 | 48 | 'html' => 'class="sql-number"', |
|||
| 238 | 48 | 'cli' => "\x1b[92m", |
|||
| 239 | 48 | 'function' => 'strtolower', |
|||
| 240 | 48 | ], |
|||
| 241 | 48 | [ |
|||
| 242 | 48 | 'type' => TokenType::String->value, |
|||
| 243 | 48 | 'flags' => 0, |
|||
| 244 | 48 | 'html' => 'class="sql-string"', |
|||
| 245 | 48 | 'cli' => "\x1b[91m", |
|||
| 246 | 48 | 'function' => '', |
|||
| 247 | 48 | ], |
|||
| 248 | 48 | [ |
|||
| 249 | 48 | 'type' => TokenType::Symbol->value, |
|||
| 250 | 48 | 'flags' => Token::FLAG_SYMBOL_PARAMETER, |
|||
| 251 | 48 | 'html' => 'class="sql-parameter"', |
|||
| 252 | 48 | 'cli' => "\x1b[31m", |
|||
| 253 | 48 | 'function' => '', |
|||
| 254 | 48 | ], |
|||
| 255 | 48 | [ |
|||
| 256 | 48 | 'type' => TokenType::Symbol->value, |
|||
| 257 | 48 | 'flags' => 0, |
|||
| 258 | 48 | 'html' => 'class="sql-variable"', |
|||
| 259 | 48 | 'cli' => "\x1b[36m", |
|||
| 260 | 48 | 'function' => '', |
|||
| 261 | 48 | ], |
|||
| 262 | 48 | ]; |
|||
| 263 | } |
||||
| 264 | |||||
| 265 | /** |
||||
| 266 | * @param array<int, array<string, int|string>> $formats |
||||
| 267 | * @param array<int, array<string, int|string>> $newFormats |
||||
| 268 | * |
||||
| 269 | * @return array<int, array<string, int|string>> |
||||
| 270 | */ |
||||
| 271 | 8 | private static function mergeFormats(array $formats, array $newFormats): array |
|||
| 272 | { |
||||
| 273 | 8 | $added = []; |
|||
| 274 | 8 | $integers = [ |
|||
| 275 | 8 | 'flags', |
|||
| 276 | 8 | 'type', |
|||
| 277 | 8 | ]; |
|||
| 278 | 8 | $strings = [ |
|||
| 279 | 8 | 'html', |
|||
| 280 | 8 | 'cli', |
|||
| 281 | 8 | 'function', |
|||
| 282 | 8 | ]; |
|||
| 283 | |||||
| 284 | /* Sanitize the array so that we do not have to care later */ |
||||
| 285 | 8 | foreach ($newFormats as $j => $new) { |
|||
| 286 | 8 | foreach ($integers as $name) { |
|||
| 287 | 8 | if (isset($new[$name])) { |
|||
| 288 | 6 | continue; |
|||
| 289 | } |
||||
| 290 | |||||
| 291 | 6 | $newFormats[$j][$name] = 0; |
|||
| 292 | } |
||||
| 293 | |||||
| 294 | 8 | foreach ($strings as $name) { |
|||
| 295 | 8 | if (isset($new[$name])) { |
|||
| 296 | 6 | continue; |
|||
| 297 | } |
||||
| 298 | |||||
| 299 | 8 | $newFormats[$j][$name] = ''; |
|||
| 300 | } |
||||
| 301 | } |
||||
| 302 | |||||
| 303 | /* Process changes to existing formats */ |
||||
| 304 | 8 | foreach ($formats as $i => $original) { |
|||
| 305 | 8 | foreach ($newFormats as $j => $new) { |
|||
| 306 | 8 | if ($new['type'] !== $original['type'] || $original['flags'] !== $new['flags']) { |
|||
| 307 | 6 | continue; |
|||
| 308 | } |
||||
| 309 | |||||
| 310 | 6 | $formats[$i] = $new; |
|||
| 311 | 6 | $added[] = $j; |
|||
| 312 | } |
||||
| 313 | } |
||||
| 314 | |||||
| 315 | /* Add not already handled formats */ |
||||
| 316 | 8 | foreach ($newFormats as $j => $new) { |
|||
| 317 | 8 | if (in_array($j, $added)) { |
|||
| 318 | 6 | continue; |
|||
| 319 | } |
||||
| 320 | |||||
| 321 | 2 | $formats[] = $new; |
|||
| 322 | } |
||||
| 323 | |||||
| 324 | 8 | return $formats; |
|||
| 325 | } |
||||
| 326 | |||||
| 327 | /** |
||||
| 328 | * Formats the given list of tokens. |
||||
| 329 | * |
||||
| 330 | * @param TokensList $list the list of tokens |
||||
| 331 | */ |
||||
| 332 | 48 | public function formatList(TokensList $list): string |
|||
| 333 | { |
||||
| 334 | /** |
||||
| 335 | * The query to be returned. |
||||
| 336 | */ |
||||
| 337 | 48 | $ret = ''; |
|||
| 338 | |||||
| 339 | /** |
||||
| 340 | * The indentation level. |
||||
| 341 | */ |
||||
| 342 | 48 | $indent = 0; |
|||
| 343 | |||||
| 344 | /** |
||||
| 345 | * Whether the line ended. |
||||
| 346 | */ |
||||
| 347 | 48 | $lineEnded = false; |
|||
| 348 | |||||
| 349 | /** |
||||
| 350 | * Whether current group is short (no linebreaks). |
||||
| 351 | */ |
||||
| 352 | 48 | $shortGroup = false; |
|||
| 353 | |||||
| 354 | /** |
||||
| 355 | * The name of the last clause. |
||||
| 356 | */ |
||||
| 357 | 48 | $lastClause = ''; |
|||
| 358 | |||||
| 359 | /** |
||||
| 360 | * A stack that keeps track of the indentation level every time a new |
||||
| 361 | * block is found. |
||||
| 362 | */ |
||||
| 363 | 48 | $blocksIndentation = []; |
|||
| 364 | |||||
| 365 | /** |
||||
| 366 | * A stack that keeps track of the line endings every time a new block |
||||
| 367 | * is found. |
||||
| 368 | */ |
||||
| 369 | 48 | $blocksLineEndings = []; |
|||
| 370 | |||||
| 371 | /** |
||||
| 372 | * Whether clause's options were formatted. |
||||
| 373 | */ |
||||
| 374 | 48 | $formattedOptions = false; |
|||
| 375 | |||||
| 376 | /** |
||||
| 377 | * Previously parsed token. |
||||
| 378 | */ |
||||
| 379 | 48 | $prev = null; |
|||
| 380 | |||||
| 381 | // In order to be able to format the queries correctly, the next token |
||||
| 382 | // must be taken into consideration. The loop below uses two pointers, |
||||
| 383 | // `$prev` and `$curr` which store two consecutive tokens. |
||||
| 384 | // Actually, at every iteration the previous token is being used. |
||||
| 385 | 48 | for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) { |
|||
| 386 | /** |
||||
| 387 | * Token parsed at this moment. |
||||
| 388 | */ |
||||
| 389 | 48 | $curr = $list->tokens[$list->idx]; |
|||
| 390 | 48 | if ($list->idx + 1 < $list->count) { |
|||
| 391 | 46 | $next = $list->tokens[$list->idx + 1]; |
|||
| 392 | } else { |
||||
| 393 | 48 | $next = null; |
|||
| 394 | } |
||||
| 395 | |||||
| 396 | 48 | if ($curr->type === TokenType::Whitespace) { |
|||
| 397 | // Keep linebreaks before and after comments |
||||
| 398 | if ( |
||||
| 399 | 46 | str_contains($curr->token, "\n") && ( |
|||
| 400 | 46 | ($prev !== null && $prev->type === TokenType::Comment) || |
|||
| 401 | 46 | ($next !== null && $next->type === TokenType::Comment) |
|||
| 402 | ) |
||||
| 403 | ) { |
||||
| 404 | 2 | $lineEnded = true; |
|||
| 405 | } |
||||
| 406 | |||||
| 407 | // Whitespaces are skipped because the formatter adds its own. |
||||
| 408 | 46 | continue; |
|||
| 409 | } |
||||
| 410 | |||||
| 411 | 48 | if ($curr->type === TokenType::Comment && $this->options['remove_comments']) { |
|||
| 412 | // Skip Comments if option `remove_comments` is enabled |
||||
| 413 | 2 | continue; |
|||
| 414 | } |
||||
| 415 | |||||
| 416 | // Checking if pointers were initialized. |
||||
| 417 | 48 | if ($prev !== null) { |
|||
| 418 | // Checking if a new clause started. |
||||
| 419 | 46 | if (static::isClause($prev) !== false) { |
|||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
| 420 | 46 | $lastClause = $prev->value; |
|||
| 421 | 46 | $formattedOptions = false; |
|||
| 422 | } |
||||
| 423 | |||||
| 424 | // The options of a clause should stay on the same line and everything that follows. |
||||
| 425 | if ( |
||||
| 426 | 46 | $this->options['parts_newline'] |
|||
| 427 | 46 | && ! $formattedOptions |
|||
| 428 | 46 | && empty(self::$inlineClauses[$lastClause]) |
|||
| 429 | && ( |
||||
| 430 | 46 | $curr->type !== TokenType::Keyword |
|||
| 431 | 46 | || ($curr->flags & Token::FLAG_KEYWORD_FUNCTION) |
|||
| 432 | ) |
||||
| 433 | ) { |
||||
| 434 | 42 | $formattedOptions = true; |
|||
| 435 | 42 | $lineEnded = true; |
|||
| 436 | 42 | ++$indent; |
|||
| 437 | } |
||||
| 438 | |||||
| 439 | // Checking if this clause ended. |
||||
| 440 | 46 | $isClause = static::isClause($curr); |
|||
| 441 | |||||
| 442 | 46 | if ($isClause !== false) { |
|||
| 443 | if ( |
||||
| 444 | 20 | ($isClause === 2 || $this->options['clause_newline']) |
|||
| 445 | 20 | && empty(self::$shortClauses[$lastClause]) |
|||
| 446 | ) { |
||||
| 447 | 20 | $lineEnded = true; |
|||
| 448 | 20 | if ($this->options['parts_newline'] && $indent > 0) { |
|||
| 449 | 18 | --$indent; |
|||
| 450 | } |
||||
| 451 | } |
||||
| 452 | } |
||||
| 453 | |||||
| 454 | // Inline JOINs |
||||
| 455 | if ( |
||||
| 456 | 46 | ($prev->type === TokenType::Keyword && isset(JoinKeyword::JOINS[$prev->value])) |
|||
| 457 | 46 | || (in_array($curr->value, ['ON', 'USING'], true) |
|||
| 458 | 46 | && isset(JoinKeyword::JOINS[$list->tokens[$list->idx - 2]->value])) |
|||
| 459 | 46 | || isset($list->tokens[$list->idx - 4], JoinKeyword::JOINS[$list->tokens[$list->idx - 4]->value]) |
|||
| 460 | 46 | || isset($list->tokens[$list->idx - 6], JoinKeyword::JOINS[$list->tokens[$list->idx - 6]->value]) |
|||
| 461 | ) { |
||||
| 462 | 2 | $lineEnded = false; |
|||
| 463 | } |
||||
| 464 | |||||
| 465 | // Indenting BEGIN ... END blocks. |
||||
| 466 | 46 | if ($prev->type === TokenType::Keyword && $prev->keyword === 'BEGIN') { |
|||
| 467 | 2 | $lineEnded = true; |
|||
| 468 | 2 | $blocksIndentation[] = $indent; |
|||
| 469 | 2 | ++$indent; |
|||
| 470 | 46 | } elseif ($curr->type === TokenType::Keyword && $curr->keyword === 'END') { |
|||
| 471 | 2 | $lineEnded = true; |
|||
| 472 | 2 | $indent = array_pop($blocksIndentation); |
|||
| 473 | } |
||||
| 474 | |||||
| 475 | // Formatting fragments delimited by comma. |
||||
| 476 | 46 | if ($prev->type === TokenType::Operator && $prev->value === ',') { |
|||
| 477 | // Fragments delimited by a comma are broken into multiple |
||||
| 478 | // pieces only if the clause is not inlined or this fragment |
||||
| 479 | // is between brackets that are on new line. |
||||
| 480 | if ( |
||||
| 481 | 8 | end($blocksLineEndings) === true |
|||
| 482 | || ( |
||||
| 483 | 8 | empty(self::$inlineClauses[$lastClause]) |
|||
| 484 | 8 | && ! $shortGroup |
|||
| 485 | 8 | && $this->options['parts_newline'] |
|||
| 486 | ) |
||||
| 487 | ) { |
||||
| 488 | 6 | $lineEnded = true; |
|||
| 489 | } |
||||
| 490 | } |
||||
| 491 | |||||
| 492 | // Handling brackets. |
||||
| 493 | // Brackets are indented only if the length of the fragment between |
||||
| 494 | // them is longer than 30 characters. |
||||
| 495 | 46 | if ($prev->type === TokenType::Operator && $prev->value === '(') { |
|||
| 496 | 12 | $blocksIndentation[] = $indent; |
|||
| 497 | 12 | $shortGroup = true; |
|||
| 498 | 12 | if (static::getGroupLength($list) > 30) { |
|||
| 499 | 2 | ++$indent; |
|||
| 500 | 2 | $lineEnded = true; |
|||
| 501 | 2 | $shortGroup = false; |
|||
| 502 | } |
||||
| 503 | |||||
| 504 | 12 | $blocksLineEndings[] = $lineEnded; |
|||
| 505 | 46 | } elseif ($curr->type === TokenType::Operator && $curr->value === ')') { |
|||
| 506 | 10 | $indent = array_pop($blocksIndentation); |
|||
| 507 | 10 | $lineEnded |= array_pop($blocksLineEndings); |
|||
| 508 | 10 | $shortGroup = false; |
|||
| 509 | } |
||||
| 510 | |||||
| 511 | // Adding the token. |
||||
| 512 | 46 | $ret .= $this->toString($prev); |
|||
|
0 ignored issues
–
show
$prev of type null is incompatible with the type PhpMyAdmin\SqlParser\Token expected by parameter $token of PhpMyAdmin\SqlParser\Utils\Formatter::toString().
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 513 | |||||
| 514 | // Finishing the line. |
||||
| 515 | 46 | if ($lineEnded) { |
|||
| 516 | 44 | $ret .= $this->options['line_ending'] . str_repeat($this->options['indentation'], (int) $indent); |
|||
| 517 | 44 | $lineEnded = false; |
|||
| 518 | } elseif ( |
||||
| 519 | 46 | $prev->keyword === 'DELIMITER' |
|||
| 520 | 46 | || ! ( |
|||
| 521 | 46 | ($prev->type === TokenType::Operator && ($prev->value === '.' || $prev->value === '(')) |
|||
| 522 | 46 | // No space after . ( |
|||
| 523 | 46 | || ($curr->type === TokenType::Operator |
|||
| 524 | 46 | && ($curr->value === '.' || $curr->value === ',' |
|||
| 525 | 46 | || $curr->value === '(' || $curr->value === ')')) |
|||
| 526 | 46 | // No space before . , ( ) |
|||
| 527 | 46 | || $curr->type === TokenType::Delimiter && mb_strlen((string) $curr->value, 'UTF-8') < 2 |
|||
| 528 | 46 | ) |
|||
| 529 | ) { |
||||
| 530 | // If the line ended, there is no point in adding whitespaces. |
||||
| 531 | // Also, some tokens do not have spaces before or after them. |
||||
| 532 | // A space after delimiters that are longer than 2 characters. |
||||
| 533 | 26 | $ret .= ' '; |
|||
| 534 | } |
||||
| 535 | } |
||||
| 536 | |||||
| 537 | // Iteration finished, consider current token as previous. |
||||
| 538 | 48 | $prev = $curr; |
|||
| 539 | } |
||||
| 540 | |||||
| 541 | 48 | if ($this->options['type'] === 'cli') { |
|||
| 542 | 40 | return $ret . "\x1b[0m"; |
|||
| 543 | } |
||||
| 544 | |||||
| 545 | 42 | return $ret; |
|||
| 546 | } |
||||
| 547 | |||||
| 548 | 38 | public function escapeConsole(string $string): string |
|||
| 549 | { |
||||
| 550 | 38 | return str_replace( |
|||
| 551 | 38 | [ |
|||
| 552 | 38 | "\x00", |
|||
| 553 | 38 | "\x01", |
|||
| 554 | 38 | "\x02", |
|||
| 555 | 38 | "\x03", |
|||
| 556 | 38 | "\x04", |
|||
| 557 | 38 | "\x05", |
|||
| 558 | 38 | "\x06", |
|||
| 559 | 38 | "\x07", |
|||
| 560 | 38 | "\x08", |
|||
| 561 | 38 | "\x09", |
|||
| 562 | 38 | "\x0A", |
|||
| 563 | 38 | "\x0B", |
|||
| 564 | 38 | "\x0C", |
|||
| 565 | 38 | "\x0D", |
|||
| 566 | 38 | "\x0E", |
|||
| 567 | 38 | "\x0F", |
|||
| 568 | 38 | "\x10", |
|||
| 569 | 38 | "\x11", |
|||
| 570 | 38 | "\x12", |
|||
| 571 | 38 | "\x13", |
|||
| 572 | 38 | "\x14", |
|||
| 573 | 38 | "\x15", |
|||
| 574 | 38 | "\x16", |
|||
| 575 | 38 | "\x17", |
|||
| 576 | 38 | "\x18", |
|||
| 577 | 38 | "\x19", |
|||
| 578 | 38 | "\x1A", |
|||
| 579 | 38 | "\x1B", |
|||
| 580 | 38 | "\x1C", |
|||
| 581 | 38 | "\x1D", |
|||
| 582 | 38 | "\x1E", |
|||
| 583 | 38 | "\x1F", |
|||
| 584 | 38 | ], |
|||
| 585 | 38 | [ |
|||
| 586 | 38 | '\x00', |
|||
| 587 | 38 | '\x01', |
|||
| 588 | 38 | '\x02', |
|||
| 589 | 38 | '\x03', |
|||
| 590 | 38 | '\x04', |
|||
| 591 | 38 | '\x05', |
|||
| 592 | 38 | '\x06', |
|||
| 593 | 38 | '\x07', |
|||
| 594 | 38 | '\x08', |
|||
| 595 | 38 | '\x09', |
|||
| 596 | 38 | '\x0A', |
|||
| 597 | 38 | '\x0B', |
|||
| 598 | 38 | '\x0C', |
|||
| 599 | 38 | '\x0D', |
|||
| 600 | 38 | '\x0E', |
|||
| 601 | 38 | '\x0F', |
|||
| 602 | 38 | '\x10', |
|||
| 603 | 38 | '\x11', |
|||
| 604 | 38 | '\x12', |
|||
| 605 | 38 | '\x13', |
|||
| 606 | 38 | '\x14', |
|||
| 607 | 38 | '\x15', |
|||
| 608 | 38 | '\x16', |
|||
| 609 | 38 | '\x17', |
|||
| 610 | 38 | '\x18', |
|||
| 611 | 38 | '\x19', |
|||
| 612 | 38 | '\x1A', |
|||
| 613 | 38 | '\x1B', |
|||
| 614 | 38 | '\x1C', |
|||
| 615 | 38 | '\x1D', |
|||
| 616 | 38 | '\x1E', |
|||
| 617 | 38 | '\x1F', |
|||
| 618 | 38 | ], |
|||
| 619 | 38 | $string, |
|||
| 620 | 38 | ); |
|||
| 621 | } |
||||
| 622 | |||||
| 623 | /** |
||||
| 624 | * Tries to print the query and returns the result. |
||||
| 625 | * |
||||
| 626 | * @param Token $token the token to be printed |
||||
| 627 | */ |
||||
| 628 | 46 | public function toString(Token $token): string |
|||
| 629 | { |
||||
| 630 | 46 | $text = $token->token; |
|||
| 631 | 46 | static $prev; |
|||
| 632 | |||||
| 633 | 46 | foreach ($this->options['formats'] as $format) { |
|||
| 634 | if ( |
||||
| 635 | 46 | $token->type->value !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags']) |
|||
| 636 | ) { |
||||
| 637 | 46 | continue; |
|||
| 638 | } |
||||
| 639 | |||||
| 640 | // Running transformation function. |
||||
| 641 | 46 | if (! empty($format['function'])) { |
|||
| 642 | 46 | $func = $format['function']; |
|||
| 643 | 46 | $text = $func($text); |
|||
| 644 | } |
||||
| 645 | |||||
| 646 | // Formatting HTML. |
||||
| 647 | 46 | if ($this->options['type'] === 'html') { |
|||
| 648 | 36 | return '<span ' . $format['html'] . '>' . htmlspecialchars($text, ENT_NOQUOTES) . '</span>'; |
|||
| 649 | } |
||||
| 650 | |||||
| 651 | 42 | if ($this->options['type'] === 'cli') { |
|||
| 652 | 38 | if ($prev !== $format['cli']) { |
|||
| 653 | 38 | $prev = $format['cli']; |
|||
| 654 | |||||
| 655 | 38 | return $format['cli'] . $this->escapeConsole($text); |
|||
| 656 | } |
||||
| 657 | |||||
| 658 | 10 | return $this->escapeConsole($text); |
|||
| 659 | } |
||||
| 660 | |||||
| 661 | 36 | break; |
|||
| 662 | } |
||||
| 663 | |||||
| 664 | 36 | if ($this->options['type'] === 'cli') { |
|||
| 665 | 28 | if ($prev !== "\x1b[39m") { |
|||
| 666 | 28 | $prev = "\x1b[39m"; |
|||
| 667 | |||||
| 668 | 28 | return "\x1b[39m" . $this->escapeConsole($text); |
|||
| 669 | } |
||||
| 670 | |||||
| 671 | 16 | return $this->escapeConsole($text); |
|||
| 672 | } |
||||
| 673 | |||||
| 674 | 36 | if ($this->options['type'] === 'html') { |
|||
| 675 | 28 | return htmlspecialchars($text, ENT_NOQUOTES); |
|||
| 676 | } |
||||
| 677 | |||||
| 678 | 36 | return $text; |
|||
| 679 | } |
||||
| 680 | |||||
| 681 | /** |
||||
| 682 | * Formats a query. |
||||
| 683 | * |
||||
| 684 | * @param string $query The query to be formatted |
||||
| 685 | * @param array<string, bool|string|array<int, array<string, int|string>>> $options the formatting options |
||||
| 686 | * |
||||
| 687 | * @return string the formatted string |
||||
| 688 | */ |
||||
| 689 | 48 | public static function format(string $query, array $options = []): string |
|||
| 690 | { |
||||
| 691 | 48 | $lexer = new Lexer($query); |
|||
| 692 | 48 | $formatter = new self($options); |
|||
| 693 | |||||
| 694 | 48 | return $formatter->formatList($lexer->list); |
|||
| 695 | } |
||||
| 696 | |||||
| 697 | /** |
||||
| 698 | * Computes the length of a group. |
||||
| 699 | * |
||||
| 700 | * A group is delimited by a pair of brackets. |
||||
| 701 | * |
||||
| 702 | * @param TokensList $list the list of tokens |
||||
| 703 | */ |
||||
| 704 | 12 | public static function getGroupLength(TokensList $list): int |
|||
| 705 | { |
||||
| 706 | /** |
||||
| 707 | * The number of opening brackets found. |
||||
| 708 | * This counter starts at one because by the time this function called, |
||||
| 709 | * the list already advanced one position and the opening bracket was |
||||
| 710 | * already parsed. |
||||
| 711 | */ |
||||
| 712 | 12 | $count = 1; |
|||
| 713 | |||||
| 714 | /** |
||||
| 715 | * The length of this group. |
||||
| 716 | */ |
||||
| 717 | 12 | $length = 0; |
|||
| 718 | |||||
| 719 | 12 | for ($idx = $list->idx; $idx < $list->count; ++$idx) { |
|||
| 720 | // Counting the brackets. |
||||
| 721 | 12 | if ($list->tokens[$idx]->type === TokenType::Operator) { |
|||
| 722 | 12 | if ($list->tokens[$idx]->value === '(') { |
|||
| 723 | 2 | ++$count; |
|||
| 724 | 12 | } elseif ($list->tokens[$idx]->value === ')') { |
|||
| 725 | 12 | --$count; |
|||
| 726 | 12 | if ($count === 0) { |
|||
| 727 | 12 | break; |
|||
| 728 | } |
||||
| 729 | } |
||||
| 730 | } |
||||
| 731 | |||||
| 732 | // Keeping track of this group's length. |
||||
| 733 | 10 | $length += mb_strlen((string) $list->tokens[$idx]->value, 'UTF-8'); |
|||
| 734 | } |
||||
| 735 | |||||
| 736 | 12 | return $length; |
|||
| 737 | } |
||||
| 738 | |||||
| 739 | /** |
||||
| 740 | * Checks if a token is a statement or a clause inside a statement. |
||||
| 741 | * |
||||
| 742 | * @param Token $token the token to be checked |
||||
| 743 | * |
||||
| 744 | * @psalm-return 1|2|false |
||||
| 745 | */ |
||||
| 746 | 46 | public static function isClause(Token $token): int|false |
|||
| 747 | { |
||||
| 748 | if ( |
||||
| 749 | 46 | ($token->type === TokenType::Keyword && isset(Parser::STATEMENT_PARSERS[$token->keyword])) |
|||
| 750 | 46 | || ($token->type === TokenType::None && strtoupper($token->token) === 'DELIMITER') |
|||
| 751 | ) { |
||||
| 752 | 44 | return 2; |
|||
| 753 | } |
||||
| 754 | |||||
| 755 | if ( |
||||
| 756 | 46 | $token->type === TokenType::Keyword && ( |
|||
| 757 | 46 | in_array($token->keyword, self::FORMATTERS, true) || isset(Parser::KEYWORD_PARSERS[$token->keyword]) |
|||
| 758 | ) |
||||
| 759 | ) { |
||||
| 760 | 20 | return 1; |
|||
| 761 | } |
||||
| 762 | |||||
| 763 | 46 | return false; |
|||
| 764 | } |
||||
| 765 | } |
||||
| 766 |