From 958bfab4ac2986ee022f0ddbee41df17f64f35c8 Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Wed, 4 Mar 2026 16:48:51 +0000 Subject: [PATCH 1/4] Refactoring Formatter Signed-off-by: Kamil Tekiela --- phpstan-baseline.neon | 86 +--------- psalm-baseline.xml | 38 +---- src/Utils/CLI.php | 6 +- src/Utils/Formatter.php | 217 ++---------------------- src/Utils/FormattingOptions.php | 25 +++ tests/Utils/FormatterTest.php | 287 +++++--------------------------- 6 files changed, 101 insertions(+), 558 deletions(-) create mode 100644 src/Utils/FormattingOptions.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 68a9a0c5..c158f719 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1753,15 +1753,15 @@ parameters: path: src/Utils/CLI.php - - message: '#^Parameter \#1 \$query of static method PhpMyAdmin\\SqlParser\\Utils\\Formatter\:\:format\(\) expects string, string\|false given\.$#' + message: '#^Parameter \#1 \$str of class PhpMyAdmin\\SqlParser\\Lexer constructor expects PhpMyAdmin\\SqlParser\\UtfString\|string, string\|false given\.$#' identifier: argument.type - count: 1 + count: 2 path: src/Utils/CLI.php - - message: '#^Parameter \#1 \$str of class PhpMyAdmin\\SqlParser\\Lexer constructor expects PhpMyAdmin\\SqlParser\\UtfString\|string, string\|false given\.$#' + message: '#^Parameter \$type of class PhpMyAdmin\\SqlParser\\Utils\\FormattingOptions constructor expects ''cli''\|''html''\|''text'', string\|false given\.$#' identifier: argument.type - count: 2 + count: 1 path: src/Utils/CLI.php - @@ -1770,52 +1770,10 @@ parameters: count: 1 path: src/Utils/Error.php - - - message: '#^Argument of an invalid type array\\>\|bool\|string supplied for foreach, only iterables are supported\.$#' - identifier: foreach.nonIterable - count: 1 - path: src/Utils/Formatter.php - - - - message: '#^Binary operation "&" between int and int\|string results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Utils/Formatter.php - - - - message: '#^Binary operation "&\=" between array\\>\|bool\|string\|null and array\\>\|bool\|string\|null results in an error\.$#' - identifier: assignOp.invalid - count: 1 - path: src/Utils/Formatter.php - - - - message: '#^Binary operation "\." between array\\>\|bool\|string and string results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Utils/Formatter.php - - - - message: '#^Binary operation "\.\=" between string and mixed results in an error\.$#' - identifier: assignOp.invalid - count: 1 - path: src/Utils/Formatter.php - - - - message: '#^Call to function in_array\(\) requires parameter \#3 to be set\.$#' - identifier: function.strict - count: 1 - path: src/Utils/Formatter.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed - count: 4 - path: src/Utils/Formatter.php - - - - message: '#^Method PhpMyAdmin\\SqlParser\\Utils\\Formatter\:\:getMergedOptions\(\) should return array\\>\|bool\|string\> but returns array\\.$#' - identifier: return.type - count: 1 + count: 3 path: src/Utils/Formatter.php - @@ -1824,30 +1782,12 @@ parameters: count: 1 path: src/Utils/Formatter.php - - - message: '#^Only booleans are allowed in &&, array\\>\|bool\|string given on the left side\.$#' - identifier: booleanAnd.leftNotBoolean - count: 2 - path: src/Utils/Formatter.php - - - - message: '#^Only booleans are allowed in &&, array\\>\|bool\|string given on the right side\.$#' - identifier: booleanAnd.rightNotBoolean - count: 2 - path: src/Utils/Formatter.php - - message: '#^Only booleans are allowed in an if condition, bool\|int given\.$#' identifier: if.condNotBoolean count: 1 path: src/Utils/Formatter.php - - - message: '#^Only booleans are allowed in \|\|, array\\>\|bool\|string given on the right side\.$#' - identifier: booleanOr.rightNotBoolean - count: 1 - path: src/Utils/Formatter.php - - message: '#^Only booleans are allowed in \|\|, int\<0, 32\> given on the right side\.$#' identifier: booleanOr.rightNotBoolean @@ -1866,24 +1806,12 @@ parameters: count: 2 path: src/Utils/Formatter.php - - - message: '#^Parameter \#1 \$string of function str_repeat expects string, array\\>\|bool\|string given\.$#' - identifier: argument.type - count: 1 - path: src/Utils/Formatter.php - - message: '#^Parameter \#1 \$string of method PhpMyAdmin\\SqlParser\\Utils\\Formatter\:\:escapeConsole\(\) expects string, mixed given\.$#' identifier: argument.type count: 4 path: src/Utils/Formatter.php - - - message: '#^Parameter \#2 \$newFormats of static method PhpMyAdmin\\SqlParser\\Utils\\Formatter\:\:mergeFormats\(\) expects array\\>, array\\>\|bool\|string given\.$#' - identifier: argument.type - count: 1 - path: src/Utils/Formatter.php - - message: '#^Possibly invalid array key type bool\|float\|int\|string\.$#' identifier: offsetAccess.invalidOffset @@ -1891,7 +1819,7 @@ parameters: path: src/Utils/Formatter.php - - message: '#^Trying to invoke int\\|int\<1, max\>\|non\-falsy\-string but it might not be a callable\.$#' + message: '#^Trying to invoke non\-empty\-string but it might not be a callable\.$#' identifier: callable.nonCallable count: 1 path: src/Utils/Formatter.php @@ -2736,7 +2664,7 @@ parameters: - message: '#^Dynamic call to static method PHPUnit\\Framework\\Assert\:\:assertEquals\(\)\.$#' identifier: staticMethod.dynamicCall - count: 4 + count: 3 path: tests/Utils/FormatterTest.php - diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 3e376fc5..e8bb68f6 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -951,6 +951,9 @@ + + + @@ -962,7 +965,7 @@ - + @@ -1008,16 +1011,6 @@ - - - - - - - - - - @@ -1030,32 +1023,16 @@ - - - - - - - - - - - - options['indentation']]]> - - - options['formats']]]> - @@ -1068,11 +1045,6 @@ - options['clause_newline']]]> - options['parts_newline']]]> - options['parts_newline']]]> - options['parts_newline']]]> - options['remove_comments']]]> diff --git a/src/Utils/CLI.php b/src/Utils/CLI.php index f0bf2c27..fbd75c13 100644 --- a/src/Utils/CLI.php +++ b/src/Utils/CLI.php @@ -139,11 +139,13 @@ public function runHighlight(): int Context::setMode(Context::SQL_MODE_ANSI_QUOTES); } - if (isset($params['q'])) { + if (isset($params['q']) && $params['q'] !== false) { + // $params['f'] is guaranteed to be set and valid at this point. @see parseHighlight() echo Formatter::format( $params['q'], - ['type' => $params['f']], + new FormattingOptions(type: $params['f']), ); + echo "\n"; return 0; diff --git a/src/Utils/Formatter.php b/src/Utils/Formatter.php index 5c9e011d..1534c267 100644 --- a/src/Utils/Formatter.php +++ b/src/Utils/Formatter.php @@ -11,7 +11,6 @@ use PhpMyAdmin\SqlParser\TokensList; use PhpMyAdmin\SqlParser\TokenType; -use function array_merge; use function array_pop; use function end; use function htmlspecialchars; @@ -23,20 +22,12 @@ use function strtoupper; use const ENT_NOQUOTES; -use const PHP_SAPI; /** * Utilities that are used for formatting queries. */ class Formatter { - /** - * The formatting options. - * - * @var array>> - */ - public array $options; - /** * Clauses that are usually short. * @@ -83,122 +74,15 @@ class Formatter 'SUBPARTITION BY', ]; - /** @param array>> $options the formatting options */ - public function __construct(array $options = []) - { - $this->options = $this->getMergedOptions($options); - } - - /** - * The specified formatting options are merged with the default values. - * - * @param array>> $options - * - * @return array>> - */ - protected function getMergedOptions(array $options): array + protected function __construct(protected FormattingOptions $options = new FormattingOptions()) { - $options = array_merge( - $this->getDefaultOptions(), - $options, - ); - - if (isset($options['formats'])) { - $options['formats'] = self::mergeFormats($this->getDefaultFormats(), $options['formats']); - } else { - $options['formats'] = $this->getDefaultFormats(); - } - - if ($options['line_ending'] === null) { - $options['line_ending'] = $options['type'] === 'html' ? '
' : "\n"; - } - - if ($options['indentation'] === null) { - $options['indentation'] = $options['type'] === 'html' ? '    ' : ' '; - } - - // `parts_newline` requires `clause_newline` - $options['parts_newline'] &= $options['clause_newline']; - - return $options; - } - - /** - * The default formatting options. - * - * @return array - * @psalm-return array{ - * type: ('cli'|'text'), - * line_ending: null, - * indentation: null, - * remove_comments: false, - * clause_newline: true, - * parts_newline: true, - * indent_parts: true - * } - */ - protected function getDefaultOptions(): array - { - return [ - /* - * The format of the result. - * - * @var string The type ('text', 'cli' or 'html') - */ - 'type' => PHP_SAPI === 'cli' ? 'cli' : 'text', - - /* - * The line ending used. - * By default, for text this is "\n" and for HTML this is "
". - * - * @var string - */ - 'line_ending' => null, - - /* - * The string used for indentation. - * - * @var string - */ - 'indentation' => null, - - /* - * Whether comments should be removed or not. - * - * @var bool - */ - 'remove_comments' => false, - - /* - * Whether each clause should be on a new line. - * - * @var bool - */ - 'clause_newline' => true, - - /* - * Whether each part should be on a new line. - * Parts are delimited by brackets and commas. - * - * @var bool - */ - 'parts_newline' => true, - - /* - * Whether each part of each clause should be indented. - * - * @var bool - */ - 'indent_parts' => true, - ]; } /** * The styles used for HTML formatting. * [$type, $flags, $span, $callback]. * - * @return array> - * @psalm-return list + * @return list */ protected function getDefaultFormats(): array { @@ -262,68 +146,6 @@ protected function getDefaultFormats(): array ]; } - /** - * @param array> $formats - * @param array> $newFormats - * - * @return array> - */ - private static function mergeFormats(array $formats, array $newFormats): array - { - $added = []; - $integers = [ - 'flags', - 'type', - ]; - $strings = [ - 'html', - 'cli', - 'function', - ]; - - /* Sanitize the array so that we do not have to care later */ - foreach ($newFormats as $j => $new) { - foreach ($integers as $name) { - if (isset($new[$name])) { - continue; - } - - $newFormats[$j][$name] = 0; - } - - foreach ($strings as $name) { - if (isset($new[$name])) { - continue; - } - - $newFormats[$j][$name] = ''; - } - } - - /* Process changes to existing formats */ - foreach ($formats as $i => $original) { - foreach ($newFormats as $j => $new) { - if ($new['type'] !== $original['type'] || $original['flags'] !== $new['flags']) { - continue; - } - - $formats[$i] = $new; - $added[] = $j; - } - } - - /* Add not already handled formats */ - foreach ($newFormats as $j => $new) { - if (in_array($j, $added)) { - continue; - } - - $formats[] = $new; - } - - return $formats; - } - /** * Formats the given list of tokens. * @@ -408,7 +230,7 @@ public function formatList(TokensList $list): string continue; } - if ($curr->type === TokenType::Comment && $this->options['remove_comments']) { + if ($curr->type === TokenType::Comment && $this->options->removeComments) { // Skip Comments if option `remove_comments` is enabled continue; } @@ -423,7 +245,7 @@ public function formatList(TokensList $list): string // The options of a clause should stay on the same line and everything that follows. if ( - $this->options['parts_newline'] + $this->options->clauseNewline && ! $formattedOptions && empty(self::$inlineClauses[$lastClause]) && ( @@ -440,12 +262,9 @@ public function formatList(TokensList $list): string $isClause = static::isClause($curr); if ($isClause !== false) { - if ( - ($isClause === 2 || $this->options['clause_newline']) - && empty(self::$shortClauses[$lastClause]) - ) { + if (($isClause === 2 || $this->options->clauseNewline) && empty(self::$shortClauses[$lastClause])) { $lineEnded = true; - if ($this->options['parts_newline'] && $indent > 0) { + if ($this->options->clauseNewline && $indent > 0) { --$indent; } } @@ -482,7 +301,7 @@ public function formatList(TokensList $list): string || ( empty(self::$inlineClauses[$lastClause]) && ! $shortGroup - && $this->options['parts_newline'] + && $this->options->clauseNewline ) ) { $lineEnded = true; @@ -513,7 +332,7 @@ public function formatList(TokensList $list): string // Finishing the line. if ($lineEnded) { - $ret .= $this->options['line_ending'] . str_repeat($this->options['indentation'], (int) $indent); + $ret .= $this->options->lineEnding . str_repeat($this->options->indentation, (int) $indent); $lineEnded = false; } elseif ( $prev->keyword === 'DELIMITER' @@ -541,7 +360,7 @@ public function formatList(TokensList $list): string $prev = $curr; } - if ($this->options['type'] === 'cli') { + if ($this->options->type === 'cli') { return $ret . "\x1b[0m"; } @@ -633,7 +452,7 @@ public function toString(Token $token): string $text = $token->token; static $prev; - foreach ($this->options['formats'] as $format) { + foreach ($this->getDefaultFormats() as $format) { if ( $token->type->value !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags']) ) { @@ -641,17 +460,17 @@ public function toString(Token $token): string } // Running transformation function. - if (! empty($format['function'])) { + if ($format['function'] !== '') { $func = $format['function']; $text = $func($text); } // Formatting HTML. - if ($this->options['type'] === 'html') { + if ($this->options->type === 'html') { return '' . htmlspecialchars($text, ENT_NOQUOTES) . ''; } - if ($this->options['type'] === 'cli') { + if ($this->options->type === 'cli') { if ($prev !== $format['cli']) { $prev = $format['cli']; @@ -664,7 +483,7 @@ public function toString(Token $token): string break; } - if ($this->options['type'] === 'cli') { + if ($this->options->type === 'cli') { if ($prev !== "\x1b[39m") { $prev = "\x1b[39m"; @@ -674,7 +493,7 @@ public function toString(Token $token): string return $this->escapeConsole($text); } - if ($this->options['type'] === 'html') { + if ($this->options->type === 'html') { return htmlspecialchars($text, ENT_NOQUOTES); } @@ -684,12 +503,12 @@ public function toString(Token $token): string /** * Formats a query. * - * @param string $query The query to be formatted - * @param array>> $options the formatting options + * @param string $query The query to be formatted + * @param FormattingOptions $options the formatting options * * @return string the formatted string */ - public static function format(string $query, array $options = []): string + public static function format(string $query, FormattingOptions $options = new FormattingOptions()): string { $lexer = new Lexer($query); $formatter = new self($options); diff --git a/src/Utils/FormattingOptions.php b/src/Utils/FormattingOptions.php new file mode 100644 index 00000000..e352ca4b --- /dev/null +++ b/src/Utils/FormattingOptions.php @@ -0,0 +1,25 @@ +lineEnding = $lineEnding ?? ($this->type === 'html' ? '
' : "\n"); + $this->indentation = $indentation ?? ($this->type === 'html' ? '    ' : ' '); + } +} diff --git a/tests/Utils/FormatterTest.php b/tests/Utils/FormatterTest.php index 57daf35e..4368cce4 100644 --- a/tests/Utils/FormatterTest.php +++ b/tests/Utils/FormatterTest.php @@ -6,276 +6,49 @@ use PhpMyAdmin\SqlParser\Tests\TestCase; use PhpMyAdmin\SqlParser\Utils\Formatter; +use PhpMyAdmin\SqlParser\Utils\FormattingOptions; use PHPUnit\Framework\Attributes\DataProvider; -use ReflectionMethod; class FormatterTest extends TestCase { - /** - * @param array> $default - * @param array> $overriding - * @param array> $expected - * @psalm-param list $default - * @psalm-param list $overriding - * @psalm-param list $expected - */ - #[DataProvider('mergeFormatsProvider')] - public function testMergeFormats(array $default, array $overriding, array $expected): void - { - $formatter = $this->createPartialMock(Formatter::class, ['getDefaultOptions', 'getDefaultFormats']); - - $formatter->expects($this->once()) - ->method('getDefaultOptions') - ->willReturn([ - 'type' => 'text', - 'line_ending' => null, - 'indentation' => null, - 'clause_newline' => null, - 'parts_newline' => null, - ]); - - $formatter->expects($this->once()) - ->method('getDefaultFormats') - ->willReturn($default); - - $expectedOptions = [ - 'type' => 'test-type', - 'line_ending' => '
', - 'indentation' => ' ', - 'clause_newline' => null, - 'parts_newline' => 0, - 'formats' => $expected, - ]; - - $overridingOptions = [ - 'type' => 'test-type', - 'line_ending' => '
', - 'formats' => $overriding, - ]; - - $reflectionMethod = new ReflectionMethod($formatter, 'getMergedOptions'); - $this->assertEquals($expectedOptions, $reflectionMethod->invoke($formatter, $overridingOptions)); - } - - /** - * @return array>>> - * @psalm-return array, - * overriding: list, - * expected: list - * }> - */ - public static function mergeFormatsProvider(): array - { - // [default[], overriding[], expected[]] - return [ - 'empty formats' => [ - 'default' => [ - [ - 'type' => 0, - 'flags' => 0, - 'html' => '', - 'cli' => '', - 'function' => '', - ], - ], - 'overriding' => [ - [], - ], - 'expected' => [ - [ - 'type' => 0, - 'flags' => 0, - 'html' => '', - 'cli' => '', - 'function' => '', - ], - ], - ], - 'no flags' => [ - 'default' => [ - [ - 'type' => 0, - 'flags' => 0, - 'html' => 'html', - 'cli' => 'cli', - ], - [ - 'type' => 0, - 'flags' => 1, - 'html' => 'html', - 'cli' => 'cli', - ], - ], - 'overriding' => [ - [ - 'type' => 0, - 'html' => 'new html', - 'cli' => 'new cli', - ], - ], - 'expected' => [ - [ - 'type' => 0, - 'flags' => 0, - 'html' => 'new html', - 'cli' => 'new cli', - 'function' => '', - ], - [ - 'type' => 0, - 'flags' => 1, - 'html' => 'html', - 'cli' => 'cli', - ], - ], - ], - 'with flags' => [ - 'default' => [ - [ - 'type' => -1, - 'flags' => 0, - 'html' => 'html', - 'cli' => 'cli', - ], - [ - 'type' => 0, - 'flags' => 0, - 'html' => 'html', - 'cli' => 'cli', - ], - [ - 'type' => 0, - 'flags' => 1, - 'html' => 'html', - 'cli' => 'cli', - ], - ], - 'overriding' => [ - [ - 'type' => 0, - 'flags' => 0, - 'html' => 'new html', - 'cli' => 'new cli', - ], - ], - 'expected' => [ - [ - 'type' => -1, - 'flags' => 0, - 'html' => 'html', - 'cli' => 'cli', - ], - [ - 'type' => 0, - 'flags' => 0, - 'html' => 'new html', - 'cli' => 'new cli', - 'function' => '', - ], - [ - 'type' => 0, - 'flags' => 1, - 'html' => 'html', - 'cli' => 'cli', - ], - ], - ], - 'with extra formats' => [ - 'default' => [ - [ - 'type' => 0, - 'flags' => 0, - 'html' => 'html', - 'cli' => 'cli', - ], - ], - 'overriding' => [ - [ - 'type' => 0, - 'flags' => 1, - 'html' => 'new html', - 'cli' => 'new cli', - ], - [ - 'type' => 1, - 'html' => 'new html', - 'cli' => 'new cli', - ], - [ - 'type' => 1, - 'flags' => 1, - 'html' => 'new html', - 'cli' => 'new cli', - ], - ], - 'expected' => [ - [ - 'type' => 0, - 'flags' => 0, - 'html' => 'html', - 'cli' => 'cli', - ], - [ - 'type' => 0, - 'flags' => 1, - 'html' => 'new html', - 'cli' => 'new cli', - 'function' => '', - ], - [ - 'type' => 1, - 'flags' => 0, - 'html' => 'new html', - 'cli' => 'new cli', - 'function' => '', - ], - [ - 'type' => 1, - 'flags' => 1, - 'html' => 'new html', - 'cli' => 'new cli', - 'function' => '', - ], - ], - ], - ]; - } - - /** @param array $options */ + /** @param array{removeComments?: bool, lineEnding?: string, indentation?: string} $options */ #[DataProvider('formatQueriesProviders')] - public function testFormat(string $query, string $text, string $cli, string $html, array $options = []): void - { - // Test TEXT format + public function testFormat( + string $query, + string $text, + string $cli, + string $html, + array $options = [], + ): void { + $options['type'] = 'text'; $this->assertEquals( $text, - Formatter::format($query, ['type' => 'text'] + $options), + Formatter::format($query, new FormattingOptions(...$options)), 'Text formatting failed.', ); - // Test CLI format + $options['type'] = 'cli'; $this->assertEquals( $cli, - Formatter::format($query, ['type' => 'cli'] + $options), + Formatter::format($query, new FormattingOptions(...$options)), 'CLI formatting failed.', ); - // Test HTML format + $options['type'] = 'html'; $this->assertEquals( $html, - Formatter::format($query, ['type' => 'html'] + $options), + Formatter::format($query, new FormattingOptions(...$options)), 'HTML formatting failed.', ); } /** - * @return array>> - * @psalm-return array + * options?: array{removeComments?: bool, lineEnding?: string, indentation?: string} * }> */ public static function formatQueriesProviders(): array @@ -440,7 +213,7 @@ public static function formatQueriesProviders(): array '    tbl
' . 'WHERE
' . '    1', - 'options' => ['remove_comments' => true], + 'options' => ['removeComments' => true], ], 'keywords' => [ 'query' => 'select hex("1")', @@ -634,6 +407,30 @@ public static function formatQueriesProviders(): array 'WHERE
' . '    col = ?', ], + 'single line' => [ + 'query' => 'select *' . "\n" . + 'from tbl # Comment' . "\n" . + 'where 1 -- Comment', + 'text' => 'SELECT ' . + '* ' . + 'FROM ' . + 'tbl ' . + 'WHERE ' . + '1', + 'cli' => "\x1b[35mSELECT " . + "\x1b[39m* " . + "\x1b[35mFROM " . + "\x1b[39mtbl " . + "\x1b[35mWHERE " . + "\x1b[92m1\x1b[0m", + 'html' => 'SELECT ' . + '* ' . + 'FROM ' . + 'tbl ' . + 'WHERE ' . + '1', + 'options' => ['lineEnding' => ' ', 'indentation' => '', 'removeComments' => true], + ], ]; } } From ed1cd527e3549568a25207352201f752905973bb Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Wed, 4 Mar 2026 17:38:35 +0000 Subject: [PATCH 2/4] Use callable Signed-off-by: Kamil Tekiela --- phpstan-baseline.neon | 6 ------ src/Utils/Formatter.php | 11 ++++++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c158f719..22a4e6a6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1818,12 +1818,6 @@ parameters: count: 7 path: src/Utils/Formatter.php - - - message: '#^Trying to invoke non\-empty\-string but it might not be a callable\.$#' - identifier: callable.nonCallable - count: 1 - path: src/Utils/Formatter.php - - message: ''' #^Access to deprecated property \$isDelete of class PhpMyAdmin\\SqlParser\\Utils\\StatementFlags\: diff --git a/src/Utils/Formatter.php b/src/Utils/Formatter.php index 1534c267..33b63eb9 100644 --- a/src/Utils/Formatter.php +++ b/src/Utils/Formatter.php @@ -19,6 +19,7 @@ use function str_contains; use function str_repeat; use function str_replace; +use function strtolower; use function strtoupper; use const ENT_NOQUOTES; @@ -82,7 +83,7 @@ protected function __construct(protected FormattingOptions $options = new Format * The styles used for HTML formatting. * [$type, $flags, $span, $callback]. * - * @return list + * @return list */ protected function getDefaultFormats(): array { @@ -92,14 +93,14 @@ protected function getDefaultFormats(): array 'flags' => Token::FLAG_KEYWORD_RESERVED, 'html' => 'class="sql-reserved"', 'cli' => "\x1b[35m", - 'function' => 'strtoupper', + 'function' => strtoupper(...), ], [ 'type' => TokenType::Keyword->value, 'flags' => 0, 'html' => 'class="sql-keyword"', 'cli' => "\x1b[95m", - 'function' => 'strtoupper', + 'function' => strtoupper(...), ], [ 'type' => TokenType::Comment->value, @@ -113,14 +114,14 @@ protected function getDefaultFormats(): array 'flags' => 0, 'html' => 'class="sql-atom"', 'cli' => "\x1b[36m", - 'function' => 'strtoupper', + 'function' => strtoupper(...), ], [ 'type' => TokenType::Number->value, 'flags' => 0, 'html' => 'class="sql-number"', 'cli' => "\x1b[92m", - 'function' => 'strtolower', + 'function' => strtolower(...), ], [ 'type' => TokenType::String->value, From 6ec07ed5487486271a75963f3d4822328493a92d Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Thu, 5 Mar 2026 14:53:14 +0000 Subject: [PATCH 3/4] Simplify getDefaultFormats Signed-off-by: Kamil Tekiela --- src/Utils/Formatter.php | 45 ++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Utils/Formatter.php b/src/Utils/Formatter.php index 33b63eb9..dab696fe 100644 --- a/src/Utils/Formatter.php +++ b/src/Utils/Formatter.php @@ -83,64 +83,64 @@ protected function __construct(protected FormattingOptions $options = new Format * The styles used for HTML formatting. * [$type, $flags, $span, $callback]. * - * @return list + * @return list */ protected function getDefaultFormats(): array { return [ [ - 'type' => TokenType::Keyword->value, + 'type' => TokenType::Keyword, 'flags' => Token::FLAG_KEYWORD_RESERVED, - 'html' => 'class="sql-reserved"', + 'html' => 'sql-reserved', 'cli' => "\x1b[35m", 'function' => strtoupper(...), ], [ - 'type' => TokenType::Keyword->value, + 'type' => TokenType::Keyword, 'flags' => 0, - 'html' => 'class="sql-keyword"', + 'html' => 'sql-keyword', 'cli' => "\x1b[95m", 'function' => strtoupper(...), ], [ - 'type' => TokenType::Comment->value, + 'type' => TokenType::Comment, 'flags' => 0, - 'html' => 'class="sql-comment"', + 'html' => 'sql-comment', 'cli' => "\x1b[37m", 'function' => '', ], [ - 'type' => TokenType::Bool->value, + 'type' => TokenType::Bool, 'flags' => 0, - 'html' => 'class="sql-atom"', + 'html' => 'sql-atom', 'cli' => "\x1b[36m", 'function' => strtoupper(...), ], [ - 'type' => TokenType::Number->value, + 'type' => TokenType::Number, 'flags' => 0, - 'html' => 'class="sql-number"', + 'html' => 'sql-number', 'cli' => "\x1b[92m", 'function' => strtolower(...), ], [ - 'type' => TokenType::String->value, + 'type' => TokenType::String, 'flags' => 0, - 'html' => 'class="sql-string"', + 'html' => 'sql-string', 'cli' => "\x1b[91m", 'function' => '', ], [ - 'type' => TokenType::Symbol->value, + 'type' => TokenType::Symbol, 'flags' => Token::FLAG_SYMBOL_PARAMETER, - 'html' => 'class="sql-parameter"', + 'html' => 'sql-parameter', 'cli' => "\x1b[31m", 'function' => '', ], [ - 'type' => TokenType::Symbol->value, + 'type' => TokenType::Symbol, 'flags' => 0, - 'html' => 'class="sql-variable"', + 'html' => 'sql-variable', 'cli' => "\x1b[36m", 'function' => '', ], @@ -454,21 +454,16 @@ public function toString(Token $token): string static $prev; foreach ($this->getDefaultFormats() as $format) { - if ( - $token->type->value !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags']) - ) { + if ($token->type !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags'])) { continue; } - // Running transformation function. if ($format['function'] !== '') { - $func = $format['function']; - $text = $func($text); + $text = $format['function']($text); } - // Formatting HTML. if ($this->options->type === 'html') { - return '' . htmlspecialchars($text, ENT_NOQUOTES) . ''; + return '' . htmlspecialchars($text, ENT_NOQUOTES) . ''; } if ($this->options->type === 'cli') { From 6bc115d7d98fe4be1184609edf9c7bb5c195f2ad Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Thu, 5 Mar 2026 15:04:54 +0000 Subject: [PATCH 4/4] Add mergeFormats() back Signed-off-by: Kamil Tekiela --- src/Utils/Formatter.php | 71 +--------------------- src/Utils/FormattingOptions.php | 104 +++++++++++++++++++++++++++++++- tests/Utils/FormatterTest.php | 59 ++++++++++++++++++ 3 files changed, 163 insertions(+), 71 deletions(-) diff --git a/src/Utils/Formatter.php b/src/Utils/Formatter.php index dab696fe..3c0da19e 100644 --- a/src/Utils/Formatter.php +++ b/src/Utils/Formatter.php @@ -19,7 +19,6 @@ use function str_contains; use function str_repeat; use function str_replace; -use function strtolower; use function strtoupper; use const ENT_NOQUOTES; @@ -79,74 +78,6 @@ protected function __construct(protected FormattingOptions $options = new Format { } - /** - * The styles used for HTML formatting. - * [$type, $flags, $span, $callback]. - * - * @return list - */ - protected function getDefaultFormats(): array - { - return [ - [ - 'type' => TokenType::Keyword, - 'flags' => Token::FLAG_KEYWORD_RESERVED, - 'html' => 'sql-reserved', - 'cli' => "\x1b[35m", - 'function' => strtoupper(...), - ], - [ - 'type' => TokenType::Keyword, - 'flags' => 0, - 'html' => 'sql-keyword', - 'cli' => "\x1b[95m", - 'function' => strtoupper(...), - ], - [ - 'type' => TokenType::Comment, - 'flags' => 0, - 'html' => 'sql-comment', - 'cli' => "\x1b[37m", - 'function' => '', - ], - [ - 'type' => TokenType::Bool, - 'flags' => 0, - 'html' => 'sql-atom', - 'cli' => "\x1b[36m", - 'function' => strtoupper(...), - ], - [ - 'type' => TokenType::Number, - 'flags' => 0, - 'html' => 'sql-number', - 'cli' => "\x1b[92m", - 'function' => strtolower(...), - ], - [ - 'type' => TokenType::String, - 'flags' => 0, - 'html' => 'sql-string', - 'cli' => "\x1b[91m", - 'function' => '', - ], - [ - 'type' => TokenType::Symbol, - 'flags' => Token::FLAG_SYMBOL_PARAMETER, - 'html' => 'sql-parameter', - 'cli' => "\x1b[31m", - 'function' => '', - ], - [ - 'type' => TokenType::Symbol, - 'flags' => 0, - 'html' => 'sql-variable', - 'cli' => "\x1b[36m", - 'function' => '', - ], - ]; - } - /** * Formats the given list of tokens. * @@ -453,7 +384,7 @@ public function toString(Token $token): string $text = $token->token; static $prev; - foreach ($this->getDefaultFormats() as $format) { + foreach ($this->options->formats as $format) { if ($token->type !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags'])) { continue; } diff --git a/src/Utils/FormattingOptions.php b/src/Utils/FormattingOptions.php index e352ca4b..16a98abb 100644 --- a/src/Utils/FormattingOptions.php +++ b/src/Utils/FormattingOptions.php @@ -4,6 +4,12 @@ namespace PhpMyAdmin\SqlParser\Utils; +use PhpMyAdmin\SqlParser\Token; +use PhpMyAdmin\SqlParser\TokenType; + +use function strtolower; +use function strtoupper; + use const PHP_SAPI; final class FormattingOptions @@ -11,15 +17,111 @@ final class FormattingOptions public string $lineEnding; public string $indentation; - /** @param 'cli'|'text'|'html' $type */ + /** + * @param 'cli'|'text'|'html' $type + * @param list $formats + */ public function __construct( public readonly string $type = PHP_SAPI === 'cli' ? 'cli' : 'text', string|null $lineEnding = null, string|null $indentation = null, public bool $removeComments = false, public bool $clauseNewline = true, + public array $formats = [], ) { $this->lineEnding = $lineEnding ?? ($this->type === 'html' ? '
' : "\n"); $this->indentation = $indentation ?? ($this->type === 'html' ? '    ' : ' '); + $this->formats = self::mergeFormats(self::getDefaultFormats(), $this->formats); + } + + /** + * @param list $formats + * @param list $newFormats + * + * @return list + */ + private static function mergeFormats(array $formats, array $newFormats): array + { + foreach ($newFormats as $new) { + foreach ($formats as $i => $original) { + if ($new['type'] !== $original['type'] || $original['flags'] !== $new['flags']) { + continue; + } + + $formats[$i] = $new; + continue 2; + } + + $formats[] = $new; + } + + return $formats; + } + + /** + * The styles used for HTML formatting. + * + * @return list + */ + public static function getDefaultFormats(): array + { + return [ + [ + 'type' => TokenType::Keyword, + 'flags' => Token::FLAG_KEYWORD_RESERVED, + 'html' => 'sql-reserved', + 'cli' => "\x1b[35m", + 'function' => strtoupper(...), + ], + [ + 'type' => TokenType::Keyword, + 'flags' => 0, + 'html' => 'sql-keyword', + 'cli' => "\x1b[95m", + 'function' => strtoupper(...), + ], + [ + 'type' => TokenType::Comment, + 'flags' => 0, + 'html' => 'sql-comment', + 'cli' => "\x1b[37m", + 'function' => '', + ], + [ + 'type' => TokenType::Bool, + 'flags' => 0, + 'html' => 'sql-atom', + 'cli' => "\x1b[36m", + 'function' => strtoupper(...), + ], + [ + 'type' => TokenType::Number, + 'flags' => 0, + 'html' => 'sql-number', + 'cli' => "\x1b[92m", + 'function' => strtolower(...), + ], + [ + 'type' => TokenType::String, + 'flags' => 0, + 'html' => 'sql-string', + 'cli' => "\x1b[91m", + 'function' => '', + ], + [ + 'type' => TokenType::Symbol, + 'flags' => Token::FLAG_SYMBOL_PARAMETER, + 'html' => 'sql-parameter', + 'cli' => "\x1b[31m", + 'function' => '', + ], + [ + 'type' => TokenType::Symbol, + 'flags' => 0, + 'html' => 'sql-variable', + 'cli' => "\x1b[36m", + 'function' => '', + ], + ]; } } diff --git a/tests/Utils/FormatterTest.php b/tests/Utils/FormatterTest.php index 4368cce4..e717fdd3 100644 --- a/tests/Utils/FormatterTest.php +++ b/tests/Utils/FormatterTest.php @@ -5,12 +5,71 @@ namespace PhpMyAdmin\SqlParser\Tests\Utils; use PhpMyAdmin\SqlParser\Tests\TestCase; +use PhpMyAdmin\SqlParser\Token; +use PhpMyAdmin\SqlParser\TokenType; use PhpMyAdmin\SqlParser\Utils\Formatter; use PhpMyAdmin\SqlParser\Utils\FormattingOptions; use PHPUnit\Framework\Attributes\DataProvider; +use function strtoupper; + class FormatterTest extends TestCase { + public function testMergeFormats(): void + { + $object = new FormattingOptions(formats: []); + self::assertEquals($object->formats, FormattingOptions::getDefaultFormats()); + + $object = new FormattingOptions(formats: [ + [ + 'type' => TokenType::Keyword, + 'flags' => Token::FLAG_KEYWORD_RESERVED, + 'html' => 'sql-foo', + 'cli' => "\x1b[35m", + 'function' => strtoupper(...), + ], + [ + 'type' => TokenType::Keyword, + 'flags' => 0, + 'html' => 'sql-bar', + 'cli' => "\x1b[95m", + 'function' => strtoupper(...), + ], + [ + 'type' => TokenType::Keyword, + 'flags' => Token::FLAG_KEYWORD_COMPOSED, + 'html' => 'sql-baz', + 'cli' => "\x1b[95m", + 'function' => strtoupper(...), + ], + + ]); + + self::assertContainsEquals([ + 'type' => TokenType::Keyword, + 'flags' => Token::FLAG_KEYWORD_RESERVED, + 'html' => 'sql-foo', + 'cli' => "\x1b[35m", + 'function' => strtoupper(...), + ], $object->formats); + + self::assertContainsEquals([ + 'type' => TokenType::Keyword, + 'flags' => 0, + 'html' => 'sql-bar', + 'cli' => "\x1b[95m", + 'function' => strtoupper(...), + ], $object->formats); + + self::assertContainsEquals([ + 'type' => TokenType::Keyword, + 'flags' => Token::FLAG_KEYWORD_COMPOSED, + 'html' => 'sql-baz', + 'cli' => "\x1b[95m", + 'function' => strtoupper(...), + ], $object->formats); + } + /** @param array{removeComments?: bool, lineEnding?: string, indentation?: string} $options */ #[DataProvider('formatQueriesProviders')] public function testFormat(