diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4a350a287..825d3e395 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -222,7 +222,7 @@ parameters: - message: "#^Cannot access offset 'expr' on mixed\\.$#" - count: 4 + count: 2 path: src/Components/OptionsArray.php - @@ -237,12 +237,12 @@ parameters: - message: "#^Cannot access offset 1 on mixed\\.$#" - count: 6 + count: 2 path: src/Components/OptionsArray.php - message: "#^Cannot access offset 2 on mixed\\.$#" - count: 2 + count: 1 path: src/Components/OptionsArray.php - @@ -370,11 +370,6 @@ parameters: count: 1 path: src/Context.php - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: src/Core.php - - message: "#^Property PhpMyAdmin\\\\SqlParser\\\\Exceptions\\\\ParserException\\:\\:\\$token \\(PhpMyAdmin\\\\SqlParser\\\\Token\\) does not accept PhpMyAdmin\\\\SqlParser\\\\Token\\|null\\.$#" count: 1 @@ -391,22 +386,22 @@ parameters: path: src/Lexer.php - - message: "#^Parameter \\#1 \\$msg \\(string\\) of method PhpMyAdmin\\\\SqlParser\\\\Lexer\\:\\:error\\(\\) should be compatible with parameter \\$error \\(Exception\\) of method PhpMyAdmin\\\\SqlParser\\\\Core\\:\\:error\\(\\)$#" + message: "#^Parameter \\#1 \\$token of class PhpMyAdmin\\\\SqlParser\\\\Token constructor expects string, null given\\.$#" count: 1 path: src/Lexer.php - - message: "#^Parameter \\#1 \\$token of class PhpMyAdmin\\\\SqlParser\\\\Token constructor expects string, null given\\.$#" + message: "#^Parameter \\#3 \\$flags of class PhpMyAdmin\\\\SqlParser\\\\Token constructor expects int, int\\|null given\\.$#" count: 1 path: src/Lexer.php - - message: "#^Parameter \\#3 \\$flags of class PhpMyAdmin\\\\SqlParser\\\\Token constructor expects int, int\\|null given\\.$#" + message: "#^Property PhpMyAdmin\\\\SqlParser\\\\Lexer\\:\\:\\$delimiter \\(string\\) does not accept null\\.$#" count: 1 path: src/Lexer.php - - message: "#^Property PhpMyAdmin\\\\SqlParser\\\\Lexer\\:\\:\\$delimiter \\(string\\) does not accept null\\.$#" + message: "#^Strict comparison using \\=\\=\\= between non\\-empty\\-array\\ and array\\{\\} will always evaluate to false\\.$#" count: 1 path: src/Lexer.php @@ -430,11 +425,6 @@ parameters: count: 4 path: src/Parser.php - - - message: "#^Parameter \\#1 \\$msg \\(string\\) of method PhpMyAdmin\\\\SqlParser\\\\Parser\\:\\:error\\(\\) should be compatible with parameter \\$error \\(Exception\\) of method PhpMyAdmin\\\\SqlParser\\\\Core\\:\\:error\\(\\)$#" - count: 1 - path: src/Parser.php - - message: "#^Parameter \\#2 \\$list of method PhpMyAdmin\\\\SqlParser\\\\Statement\\:\\:validateClauseOrder\\(\\) expects PhpMyAdmin\\\\SqlParser\\\\TokensList, PhpMyAdmin\\\\SqlParser\\\\TokensList\\|null given\\.$#" count: 3 @@ -445,6 +435,11 @@ parameters: count: 1 path: src/Parser.php + - + message: "#^Strict comparison using \\=\\=\\= between non\\-empty\\-array\\ and array\\{\\} will always evaluate to false\\.$#" + count: 1 + path: src/Parser.php + - message: "#^Call to an undefined static method PhpMyAdmin\\\\SqlParser\\\\Component\\:\\:buildAll\\(\\)\\.$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 93e6d11b6..f301a79ec 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $options @@ -93,9 +93,6 @@ $prev[0] === null - - - keyword]]]> keyword]]]> @@ -120,9 +117,6 @@ $prev[1] !== null function) && ($prev[1] !== null)]]> - - - @@ -456,9 +450,6 @@ self::$keywords - - getIdentifierQuote - @@ -570,12 +561,6 @@ ContextMySql80100 - - - Context::$keywords !== [] - Context::$keywords !== [] - - $token @@ -595,9 +580,6 @@ null - - $msg - $flags str[$this->last + 1]]]> @@ -664,9 +646,6 @@ keyword]]]> keyword]]]> - - $msg - $list $list @@ -1013,16 +992,13 @@ - + - + ++$i !== $count ++$i !== $count - - string - @@ -1220,9 +1196,6 @@ has has - - - @@ -1265,7 +1238,6 @@ $ret $ret $ret - $ret has @@ -1603,108 +1575,6 @@ statements[0]]]> statements[0]]]> - - [ - 'type' => 'SMALLINT', - 'timestamp_not_null' => false, - ], - 'address' => [ - 'type' => 'VARCHAR', - 'timestamp_not_null' => false, - ], - 'address2' => [ - 'type' => 'VARCHAR', - 'timestamp_not_null' => false, - 'default_value' => 'NULL', - ], - 'district' => [ - 'type' => 'VARCHAR', - 'timestamp_not_null' => false, - ], - 'city_id' => [ - 'type' => 'SMALLINT', - 'timestamp_not_null' => false, - ], - 'postal_code' => [ - 'type' => 'VARCHAR', - 'timestamp_not_null' => false, - 'default_value' => 'NULL', - ], - 'phone' => [ - 'type' => 'VARCHAR', - 'timestamp_not_null' => false, - ], - 'last_update' => [ - 'type' => 'TIMESTAMP', - 'timestamp_not_null' => true, - 'default_value' => 'CURRENT_TIMESTAMP', - 'default_current_timestamp' => true, - 'on_update_current_timestamp' => true, - ], - ], - ], - [ - 'CREATE TABLE table1 ( - a INT NOT NULL, - b VARCHAR(32), - c INT AS (a mod 10) VIRTUAL, - d VARCHAR(5) AS (left(b,5)) PERSISTENT - )', - [ - 'a' => [ - 'type' => 'INT', - 'timestamp_not_null' => false, - ], - 'b' => [ - 'type' => 'VARCHAR', - 'timestamp_not_null' => false, - ], - 'c' => [ - 'type' => 'INT', - 'timestamp_not_null' => false, - 'generated' => true, - 'expr' => '(a mod 10)', - ], - 'd' => [ - 'type' => 'VARCHAR', - 'timestamp_not_null' => false, - 'generated' => true, - 'expr' => '(left(b,5))', - ], - ], - ], - ]]]> - - - }>]]> - getFieldsProvider getForeignKeysProvider diff --git a/src/Components/GroupKeyword.php b/src/Components/GroupKeyword.php index d26475b9d..529af7143 100644 --- a/src/Components/GroupKeyword.php +++ b/src/Components/GroupKeyword.php @@ -17,8 +17,8 @@ */ final class GroupKeyword implements Component { - /** @var mixed */ - public $type; + /** @var 'ASC'|'DESC'|null */ + public string|null $type = null; /** * The expression that is used for grouping. diff --git a/src/Context.php b/src/Context.php index 068a0931c..750b616f9 100644 --- a/src/Context.php +++ b/src/Context.php @@ -705,16 +705,6 @@ public static function escapeAll(array $strings): array return $strings; } - /** - * Returns char used to quote identifiers based on currently set SQL Mode (ie. standard or ANSI_QUOTES) - * - * @return string either " (double quote, ansi_quotes mode) or ` (backtick, standard mode) - */ - public static function getIdentifierQuote(): string - { - return self::hasMode(self::SQL_MODE_ANSI_QUOTES) ? '"' : '`'; - } - /** * Function verifies that given SQL Mode constant is currently set * diff --git a/src/Core.php b/src/Core.php deleted file mode 100644 index 775b686ac..000000000 --- a/src/Core.php +++ /dev/null @@ -1,60 +0,0 @@ -strict) { - throw $error; - } - - $this->errors[] = $error; - } -} diff --git a/src/Lexer.php b/src/Lexer.php index d965bd8d6..ba1d1d0f4 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -4,6 +4,7 @@ namespace PhpMyAdmin\SqlParser; +use Exception; use PhpMyAdmin\SqlParser\Exceptions\LexerException; use function in_array; @@ -26,8 +27,24 @@ * * @see Context */ -class Lexer extends Core +class Lexer { + /** + * Whether errors should throw exceptions or just be stored. + */ + private bool $strict = false; + + /** + * List of errors that occurred during lexing. + * + * Usually, the lexing does not stop once an error occurred because that + * error might be false positive or a partial result (even a bad one) + * might be needed. + * + * @var Exception[] + */ + public array $errors = []; + /** * A list of keywords that indicate that the function keyword * is not used as a function @@ -112,7 +129,9 @@ class Lexer extends Core */ public function __construct($str, $strict = false, $delimiter = null) { - parent::__construct(); + if (Context::$keywords === []) { + Context::load(); + } // `strlen` is used instead of `mb_strlen` because the lexer needs to // parse each byte of the input. @@ -372,7 +391,12 @@ public function error($msg, $str = '', $pos = 0, $code = 0): void $pos, $code ); - parent::error($error); + + if ($this->strict) { + throw $error; + } + + $this->errors[] = $error; } /** diff --git a/src/Parser.php b/src/Parser.php index d82cb22c3..996d99d18 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -4,6 +4,7 @@ namespace PhpMyAdmin\SqlParser; +use Exception; use PhpMyAdmin\SqlParser\Exceptions\ParserException; use PhpMyAdmin\SqlParser\Statements\SelectStatement; use PhpMyAdmin\SqlParser\Statements\TransactionStatement; @@ -18,8 +19,24 @@ * * Takes multiple tokens (contained in a Lexer instance) as input and builds a parse tree. */ -class Parser extends Core +class Parser { + /** + * Whether errors should throw exceptions or just be stored. + */ + private bool $strict = false; + + /** + * List of errors that occurred during lexing. + * + * Usually, the lexing does not stop once an error occurred because that + * error might be false positive or a partial result (even a bad one) + * might be needed. + * + * @var Exception[] + */ + public array $errors = []; + /** * Array of classes that are used in parsing the SQL statements. * @@ -362,7 +379,9 @@ class Parser extends Core */ public function __construct($list = null, $strict = false) { - parent::__construct(); + if (Context::$keywords === []) { + Context::load(); + } if (is_string($list) || ($list instanceof UtfString)) { $lexer = new Lexer($list, $strict); @@ -622,6 +641,11 @@ public function error($msg, Token|null $token = null, $code = 0): void $token, $code ); - parent::error($error); + + if ($this->strict) { + throw $error; + } + + $this->errors[] = $error; } } diff --git a/src/TokensList.php b/src/TokensList.php index 5551b21ea..5b48f5fe0 100644 --- a/src/TokensList.php +++ b/src/TokensList.php @@ -190,7 +190,7 @@ public function getNextOfTypeAndFlag(TokenType $type, int $flag): Token|null * @param int|null $offset the offset to be set * @param Token $value the token to be saved */ - public function offsetSet($offset, $value): void + public function offsetSet(mixed $offset, mixed $value): void { if ($offset === null) { $this->tokens[$this->count++] = $value; @@ -204,7 +204,7 @@ public function offsetSet($offset, $value): void * * @param int $offset the offset to be returned */ - public function offsetGet($offset): Token|null + public function offsetGet(mixed $offset): Token|null { return $offset < $this->count ? $this->tokens[$offset] : null; } @@ -214,7 +214,7 @@ public function offsetGet($offset): Token|null * * @param int $offset the offset to be checked */ - public function offsetExists($offset): bool + public function offsetExists(mixed $offset): bool { return $offset < $this->count; } @@ -224,7 +224,7 @@ public function offsetExists($offset): bool * * @param int $offset the offset to be unset */ - public function offsetUnset($offset): void + public function offsetUnset(mixed $offset): void { unset($this->tokens[$offset]); --$this->count; diff --git a/src/Tools/ContextGenerator.php b/src/Tools/ContextGenerator.php index 58d36ab72..4677123cb 100644 --- a/src/Tools/ContextGenerator.php +++ b/src/Tools/ContextGenerator.php @@ -19,11 +19,11 @@ use function scandir; use function sort; use function sprintf; +use function str_contains; use function str_repeat; use function str_replace; use function str_split; use function strlen; -use function strstr; use function strtoupper; use function substr; use function trim; @@ -170,7 +170,7 @@ public static function readWords(array $files): array // Reserved, data types, keys, functions, etc. keywords. foreach (static::$labelsFlags as $label => $flags) { - if (strstr($value, $label) === false) { + if (! str_contains($value, $label)) { continue; } @@ -179,7 +179,7 @@ public static function readWords(array $files): array } // Composed keyword. - if (strstr($value, ' ') !== false) { + if (str_contains($value, ' ')) { $type |= 2; // Reserved keyword. $type |= 4; // Composed keyword. } @@ -335,8 +335,6 @@ public static function build($input, $output): void * The directory that contains the input file. * * Used to include common files. - * - * @var string */ $directory = dirname($input) . '/'; @@ -347,15 +345,11 @@ public static function build($input, $output): void /** * The name of the context. - * - * @var string */ $name = substr($file, 0, -4); /** * The name of the class that defines this context. - * - * @var string */ $class = 'Context' . $name; diff --git a/src/UtfString.php b/src/UtfString.php index 2664efb09..529db2e5c 100644 --- a/src/UtfString.php +++ b/src/UtfString.php @@ -90,7 +90,7 @@ public function __construct($str) * * @param int $offset the offset to be checked */ - public function offsetExists($offset): bool + public function offsetExists(mixed $offset): bool { return ($offset >= 0) && ($offset < $this->charLen); } @@ -100,7 +100,7 @@ public function offsetExists($offset): bool * * @param int $offset the offset to be returned */ - public function offsetGet($offset): string|null + public function offsetGet(mixed $offset): string|null { // This function moves the internal byte and character pointer to the requested offset. // This function is part of hot code so the aim is to do the following @@ -143,7 +143,7 @@ public function offsetGet($offset): string|null * * @throws Exception not implemented. */ - public function offsetSet($offset, $value): void + public function offsetSet(mixed $offset, mixed $value): void { throw new Exception('Not implemented.'); } @@ -155,7 +155,7 @@ public function offsetSet($offset, $value): void * * @throws Exception not implemented. */ - public function offsetUnset($offset): void + public function offsetUnset(mixed $offset): void { throw new Exception('Not implemented.'); } diff --git a/src/Utils/Routine.php b/src/Utils/Routine.php index d4ad5115c..343248fcb 100644 --- a/src/Utils/Routine.php +++ b/src/Utils/Routine.php @@ -37,8 +37,6 @@ public static function getReturnType($param): array '', '', '', - '', - '', ]; } @@ -48,8 +46,6 @@ public static function getReturnType($param): array } return [ - '', - '', $type->name, implode(',', $type->parameters), implode(' ', $options), diff --git a/src/Utils/Table.php b/src/Utils/Table.php index fe4082543..ccf3859e9 100644 --- a/src/Utils/Table.php +++ b/src/Utils/Table.php @@ -17,11 +17,18 @@ class Table /** * Gets the foreign keys of the table. * - * @param CreateStatement $statement the statement to be processed - * - * @return array> + * @return list<(string|string[]|null)[]> + * @psalm-return list */ - public static function getForeignKeys($statement): array + public static function getForeignKeys(CreateStatement $statement): array { if (empty($statement->fields) || (! is_array($statement->fields)) || (! $statement->options->has('TABLE'))) { return []; @@ -78,6 +85,14 @@ public static function getForeignKeys($statement): array * @param CreateStatement $statement the statement to be processed * * @return array> + * @psalm-return array */ public static function getFields($statement): array { @@ -129,7 +144,6 @@ public static function getFields($statement): array continue; } - $ret[$field->name]['generated'] = true; $ret[$field->name]['expr'] = $option; } diff --git a/tests/Lexer/LexerTest.php b/tests/Lexer/LexerTest.php index a99654567..61ab2b826 100644 --- a/tests/Lexer/LexerTest.php +++ b/tests/Lexer/LexerTest.php @@ -43,8 +43,7 @@ public function testErrorStrict(): void $this->expectExceptionCode(4); $this->expectExceptionMessage('strict error'); $this->expectException(LexerException::class); - $lexer = new Lexer(''); - $lexer->strict = true; + $lexer = new Lexer('', true); $lexer->error('strict error', 'foo', 1, 4); } diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 694624dfb..79f142be9 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -76,8 +76,7 @@ public function testErrorStrict(): void $this->expectExceptionCode(3); $this->expectExceptionMessage('strict error'); $this->expectException(ParserException::class); - $parser = new Parser(new TokensList()); - $parser->strict = true; + $parser = new Parser(new TokensList(), true); $parser->error('strict error', new Token('foo'), 3); } diff --git a/tests/Utils/RoutineTest.php b/tests/Utils/RoutineTest.php index 754484a7a..d6e0b97d5 100644 --- a/tests/Utils/RoutineTest.php +++ b/tests/Utils/RoutineTest.php @@ -33,15 +33,11 @@ public static function getReturnTypeProvider(): array '', '', '', - '', - '', ], ], [ 'TEXT', [ - '', - '', 'TEXT', '', '', @@ -50,8 +46,6 @@ public static function getReturnTypeProvider(): array [ 'INT(20)', [ - '', - '', 'INT', '20', '', @@ -60,8 +54,6 @@ public static function getReturnTypeProvider(): array [ 'INT UNSIGNED', [ - '', - '', 'INT', '', 'UNSIGNED', @@ -70,8 +62,6 @@ public static function getReturnTypeProvider(): array [ 'VARCHAR(1) CHARSET utf8', [ - '', - '', 'VARCHAR', '1', 'utf8', @@ -80,8 +70,6 @@ public static function getReturnTypeProvider(): array [ 'ENUM(\'a\', \'b\') CHARSET latin1', [ - '', - '', 'ENUM', '\'a\',\'b\'', 'latin1', @@ -90,8 +78,6 @@ public static function getReturnTypeProvider(): array [ 'DECIMAL(5,2) UNSIGNED ZEROFILL', [ - '', - '', 'DECIMAL', '5,2', 'UNSIGNED ZEROFILL', @@ -100,8 +86,6 @@ public static function getReturnTypeProvider(): array [ 'SET(\'test\'\'esc"\', \'more\\\'esc\')', [ - '', - '', 'SET', '\'test\'\'esc"\',\'more\\\'esc\'', '', diff --git a/tests/Utils/TableTest.php b/tests/Utils/TableTest.php index 53b8648fe..ac48e59e4 100644 --- a/tests/Utils/TableTest.php +++ b/tests/Utils/TableTest.php @@ -245,13 +245,11 @@ public static function getFieldsProvider(): array 'c' => [ 'type' => 'INT', 'timestamp_not_null' => false, - 'generated' => true, 'expr' => '(a mod 10)', ], 'd' => [ 'type' => 'VARCHAR', 'timestamp_not_null' => false, - 'generated' => true, 'expr' => '(left(b,5))', ], ],