diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cc7bb264..0f10403a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,7 +9,7 @@ parameters: - message: '#^Cannot call method remove\(\) on PhpMyAdmin\\SqlParser\\Components\\OptionsArray\|null\.$#' identifier: method.nonObject - count: 3 + count: 1 path: src/Components/AlterOperation.php - @@ -156,36 +156,6 @@ parameters: count: 1 path: src/Context.php - - - message: '#^Binary operation "\." between mixed and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 2 - path: src/Lexer.php - - - - message: '#^Binary operation "\." between string and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 2 - path: src/Lexer.php - - - - message: '#^Binary operation "\.\=" between mixed and mixed results in an error\.$#' - identifier: assignOp.invalid - count: 9 - path: src/Lexer.php - - - - message: '#^Binary operation "\.\=" between mixed and string results in an error\.$#' - identifier: assignOp.invalid - count: 2 - path: src/Lexer.php - - - - message: '#^Binary operation "\.\=" between string and mixed results in an error\.$#' - identifier: assignOp.invalid - count: 11 - path: src/Lexer.php - - message: '#^Cannot access property \$type on PhpMyAdmin\\SqlParser\\Token\|null\.$#' identifier: property.nonObject @@ -240,54 +210,6 @@ parameters: count: 1 path: src/Lexer.php - - - message: '#^Parameter \#1 \$character of static method PhpMyAdmin\\SqlParser\\Context\:\:isString\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Lexer.php - - - - message: '#^Parameter \#1 \$character of static method PhpMyAdmin\\SqlParser\\Context\:\:isSymbol\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Lexer.php - - - - message: '#^Parameter \#1 \$character of static method PhpMyAdmin\\SqlParser\\Context\:\:isWhitespace\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 5 - path: src/Lexer.php - - - - message: '#^Parameter \#1 \$string of static method PhpMyAdmin\\SqlParser\\Context\:\:isComment\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Lexer.php - - - - message: '#^Parameter \#1 \$string of static method PhpMyAdmin\\SqlParser\\Context\:\:isSeparator\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 4 - path: src/Lexer.php - - - - message: '#^Parameter \#1 \$token of class PhpMyAdmin\\SqlParser\\Token constructor expects string, mixed given\.$#' - identifier: argument.type - count: 7 - path: src/Lexer.php - - - - message: '#^Parameter \#2 \$str of method PhpMyAdmin\\SqlParser\\Lexer\:\:error\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Lexer.php - - - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Lexer.php - - message: '#^Property PhpMyAdmin\\SqlParser\\Token\:\:\$keyword \(string\|null\) does not accept bool\|float\|int\|string\.$#' identifier: assign.propertyType @@ -876,6 +798,12 @@ parameters: count: 1 path: src/Parsers/OptionsArrays.php + - + message: '#^Offset ''value'' might not exist on array\{expr\: string\}\|array\{name\: string, equals\: bool, expr\: string, value\: string\|null\}\.$#' + identifier: offsetAccess.notFound + count: 1 + path: src/Parsers/OptionsArrays.php + - message: '#^Only booleans are allowed in &&, mixed given on the right side\.$#' identifier: booleanAnd.rightNotBoolean @@ -889,21 +817,39 @@ parameters: path: src/Parsers/OptionsArrays.php - - message: '#^Property PhpMyAdmin\\SqlParser\\Components\\OptionsArray\:\:\$options \(array\\) does not accept non\-empty\-array\\.$#' + message: '#^Possibly invalid array key type mixed\.$#' + identifier: offsetAccess.invalidOffset + count: 14 + path: src/Parsers/OptionsArrays.php + + - + message: '#^Property PhpMyAdmin\\SqlParser\\Components\\OptionsArray\:\:\$options \(array\\) does not accept non\-empty\-array\\.$#' identifier: assign.propertyType - count: 2 + count: 1 path: src/Parsers/OptionsArrays.php - - message: '#^Property PhpMyAdmin\\SqlParser\\Components\\OptionsArray\:\:\$options \(array\\) does not accept non\-empty\-array\\.$#' + message: '#^Property PhpMyAdmin\\SqlParser\\Components\\OptionsArray\:\:\$options \(array\\) does not accept non\-empty\-array\\.$#' identifier: assign.propertyType count: 1 path: src/Parsers/OptionsArrays.php - - message: '#^Property PhpMyAdmin\\SqlParser\\Components\\OptionsArray\:\:\$options \(array\\) does not accept non\-empty\-array\\.$#' + message: '#^Property PhpMyAdmin\\SqlParser\\Components\\OptionsArray\:\:\$options \(array\\) does not accept non\-empty\-array\\.$#' identifier: assign.propertyType - count: 3 + count: 1 + path: src/Parsers/OptionsArrays.php + + - + message: '#^Property PhpMyAdmin\\SqlParser\\Components\\OptionsArray\:\:\$options \(array\\) does not accept non\-empty\-array\\.$#' + identifier: assign.propertyType + count: 2 + path: src/Parsers/OptionsArrays.php + + - + message: '#^Property PhpMyAdmin\\SqlParser\\Components\\OptionsArray\:\:\$options \(array\\) does not accept non\-empty\-array\\.$#' + identifier: assign.propertyType + count: 1 path: src/Parsers/OptionsArrays.php - @@ -1129,15 +1075,9 @@ parameters: path: src/Statement.php - - message: '#^Only booleans are allowed in an if condition, int\<0, 1\> given\.$#' - identifier: if.condNotBoolean - count: 1 - path: src/Statement.php - - - - message: '#^Only booleans are allowed in an if condition, int\<0, 2\> given\.$#' + message: '#^Only booleans are allowed in an if condition, int given\.$#' identifier: if.condNotBoolean - count: 1 + count: 2 path: src/Statement.php - @@ -1152,12 +1092,6 @@ parameters: count: 1 path: src/Statement.php - - - message: '#^Variable property access on \$this\(PhpMyAdmin\\SqlParser\\Statement\)\.$#' - identifier: property.dynamicName - count: 5 - path: src/Statement.php - - message: '#^Argument of an invalid type array\\|null supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable @@ -1290,12 +1224,6 @@ parameters: count: 1 path: src/Statements/CreateStatement.php - - - message: '#^Variable property access on \$this\(PhpMyAdmin\\SqlParser\\Statements\\CreateStatement\)\.$#' - identifier: property.dynamicName - count: 3 - path: src/Statements/CreateStatement.php - - message: '#^Argument of an invalid type array\\|null supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable @@ -1590,12 +1518,6 @@ parameters: count: 1 path: src/Statements/SelectStatement.php - - - message: '#^Variable property access on \$this\(PhpMyAdmin\\SqlParser\\Statements\\SelectStatement\)\.$#' - identifier: property.dynamicName - count: 1 - path: src/Statements/SelectStatement.php - - message: '#^Cannot call method build\(\) on PhpMyAdmin\\SqlParser\\Components\\OptionsArray\|null\.$#' identifier: method.nonObject @@ -1656,6 +1578,18 @@ parameters: count: 1 path: src/Statements/WithStatement.php + - + message: '#^Possibly invalid array key type bool\|float\|int\|string\.$#' + identifier: offsetAccess.invalidOffset + count: 1 + path: src/Statements/WithStatement.php + + - + message: '#^Possibly invalid array key type bool\|float\|int\|string\|null\.$#' + identifier: offsetAccess.invalidOffset + count: 2 + path: src/Statements/WithStatement.php + - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed @@ -1813,15 +1747,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 - @@ -1830,52 +1764,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 - @@ -1884,30 +1776,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 @@ -1926,12 +1800,6 @@ 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 @@ -1939,15 +1807,9 @@ parameters: 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: '#^Trying to invoke int\\|int\<1, max\>\|non\-falsy\-string but it might not be a callable\.$#' - identifier: callable.nonCallable - count: 1 + message: '#^Possibly invalid array key type bool\|float\|int\|string\.$#' + identifier: offsetAccess.invalidOffset + count: 7 path: src/Utils/Formatter.php - @@ -2796,7 +2658,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 c565453c..5e28f0f1 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -185,6 +185,7 @@ tokens]]> + idx]]> @@ -337,6 +338,9 @@ + + + expr]]> expr]]> @@ -395,6 +399,10 @@ + + + + type)]]> @@ -550,6 +558,9 @@ ]]> + + + @@ -799,6 +810,7 @@ + @@ -942,6 +954,9 @@ + + + @@ -953,7 +968,7 @@ - + @@ -999,16 +1014,6 @@ - - - - - - - - - - @@ -1021,32 +1026,20 @@ - - - - - - - - - - - - options['indentation']]]> - - - options['formats']]]> - + + + + @@ -1055,11 +1048,6 @@ - options['clause_newline']]]> - options['parts_newline']]]> - options['parts_newline']]]> - options['parts_newline']]]> - options['remove_comments']]]> @@ -1094,6 +1082,9 @@ + + + tokens]]> tokens]]> 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 67214f2b..98aa055e 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,245 +74,8 @@ 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 - { - $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 - */ - protected function getDefaultFormats(): array - { - return [ - [ - 'type' => TokenType::Keyword->value, - 'flags' => Token::FLAG_KEYWORD_RESERVED, - 'html' => 'class="sql-reserved"', - 'cli' => "\x1b[35m", - 'function' => 'strtoupper', - ], - [ - 'type' => TokenType::Keyword->value, - 'flags' => 0, - 'html' => 'class="sql-keyword"', - 'cli' => "\x1b[95m", - 'function' => 'strtoupper', - ], - [ - 'type' => TokenType::Comment->value, - 'flags' => 0, - 'html' => 'class="sql-comment"', - 'cli' => "\x1b[37m", - 'function' => '', - ], - [ - 'type' => TokenType::Bool->value, - 'flags' => 0, - 'html' => 'class="sql-atom"', - 'cli' => "\x1b[36m", - 'function' => 'strtoupper', - ], - [ - 'type' => TokenType::Number->value, - 'flags' => 0, - 'html' => 'class="sql-number"', - 'cli' => "\x1b[92m", - 'function' => 'strtolower', - ], - [ - 'type' => TokenType::String->value, - 'flags' => 0, - 'html' => 'class="sql-string"', - 'cli' => "\x1b[91m", - 'function' => '', - ], - [ - 'type' => TokenType::Symbol->value, - 'flags' => Token::FLAG_SYMBOL_PARAMETER, - 'html' => 'class="sql-parameter"', - 'cli' => "\x1b[31m", - 'function' => '', - ], - [ - 'type' => TokenType::Symbol->value, - 'flags' => 0, - 'html' => 'class="sql-variable"', - 'cli' => "\x1b[36m", - 'function' => '', - ], - ]; - } - - /** - * @param array> $formats - * @param array> $newFormats - * - * @return array> - */ - private static function mergeFormats(array $formats, array $newFormats): array + protected function __construct(protected FormattingOptions $options = new FormattingOptions()) { - $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; } /** @@ -408,7 +162,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 +177,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 +194,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 +233,7 @@ public function formatList(TokensList $list): string || ( empty(self::$inlineClauses[$lastClause]) && ! $shortGroup - && $this->options['parts_newline'] + && $this->options->clauseNewline ) ) { $lineEnded = true; @@ -513,7 +264,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' @@ -538,7 +289,7 @@ public function formatList(TokensList $list): string $prev = $curr; } - if ($this->options['type'] === 'cli') { + if ($this->options->type === 'cli') { return $ret . "\x1b[0m"; } @@ -630,25 +381,20 @@ public function toString(Token $token): string $text = $token->token; static $prev; - foreach ($this->options['formats'] as $format) { - if ( - $token->type->value !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags']) - ) { + foreach ($this->options->formats as $format) { + if ($token->type !== $format['type'] || ! (($token->flags & $format['flags']) === $format['flags'])) { continue; } - // Running transformation function. - if (! empty($format['function'])) { - $func = $format['function']; - $text = $func($text); + if ($format['function'] !== '') { + $text = $format['function']($text); } - // Formatting HTML. - if ($this->options['type'] === 'html') { - return '' . htmlspecialchars($text, ENT_NOQUOTES) . ''; + 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']; @@ -661,7 +407,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"; @@ -671,7 +417,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); } @@ -681,12 +427,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..16a98abb --- /dev/null +++ b/src/Utils/FormattingOptions.php @@ -0,0 +1,127 @@ + $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 557676fa..3ac4eb0c 100644 --- a/tests/Utils/FormatterTest.php +++ b/tests/Utils/FormatterTest.php @@ -5,277 +5,109 @@ 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 ReflectionMethod; + +use function strtoupper; 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 + public function testMergeFormats(): void { - $formatter = $this->createPartialMock(Formatter::class, ['getDefaultOptions', 'getDefaultFormats']); + $object = new FormattingOptions(formats: []); + self::assertEquals($object->formats, FormattingOptions::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); + $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(...), + ], - $expectedOptions = [ - 'type' => 'test-type', - 'line_ending' => '
', - 'indentation' => ' ', - 'clause_newline' => null, - 'parts_newline' => 0, - 'formats' => $expected, - ]; + ]); - $overridingOptions = [ - 'type' => 'test-type', - 'line_ending' => '
', - 'formats' => $overriding, - ]; + self::assertContainsEquals([ + 'type' => TokenType::Keyword, + 'flags' => Token::FLAG_KEYWORD_RESERVED, + 'html' => 'sql-foo', + 'cli' => "\x1b[35m", + 'function' => strtoupper(...), + ], $object->formats); - $reflectionMethod = new ReflectionMethod($formatter, 'getMergedOptions'); - $this->assertEquals($expectedOptions, $reflectionMethod->invoke($formatter, $overridingOptions)); - } + self::assertContainsEquals([ + 'type' => TokenType::Keyword, + 'flags' => 0, + 'html' => 'sql-bar', + 'cli' => "\x1b[95m", + 'function' => strtoupper(...), + ], $object->formats); - /** - * @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' => '', - ], - ], - ], - ]; + self::assertContainsEquals([ + 'type' => TokenType::Keyword, + 'flags' => Token::FLAG_KEYWORD_COMPOSED, + 'html' => 'sql-baz', + 'cli' => "\x1b[95m", + 'function' => strtoupper(...), + ], $object->formats); } - /** @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 @@ -410,7 +242,7 @@ public static function formatQueriesProviders(): array '    tbl
' . 'WHERE
' . '    1', - 'options' => ['remove_comments' => true], + 'options' => ['removeComments' => true], ], 'keywords' => [ 'query' => 'select hex("1")', @@ -604,6 +436,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], + ], ]; } }