diff --git a/composer.json b/composer.json index c899122c..e575f01f 100644 --- a/composer.json +++ b/composer.json @@ -12,18 +12,19 @@ } ], "require": { - "wp-cli/wp-cli": "^2.12" + "wp-cli/wp-cli": "^2.13" }, "require-dev": { "wp-cli/entity-command": "^1.3 || ^2", - "wp-cli/wp-cli-tests": "^4" + "wp-cli/wp-cli-tests": "^5" }, "config": { "process-timeout": 7200, "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "johnpbloch/wordpress-core-installer": true + "johnpbloch/wordpress-core-installer": true, + "phpstan/extension-installer": true }, "lock": false }, @@ -67,6 +68,7 @@ "behat-rerun": "rerun-behat-tests", "lint": "run-linter-tests", "phpcs": "run-phpcs-tests", + "phpstan": "run-phpstan-tests", "phpcbf": "run-phpcbf-cleanup", "phpunit": "run-php-unit-tests", "prepare-tests": "install-package-tests", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index c995d59c..c39cec40 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -55,4 +55,6 @@ */src/DB_Command\.php$ + + /tests/phpstan/scan-files diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..7d87518e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,22 @@ +parameters: + level: 9 + paths: + - src + - db-command.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - tests/phpstan/scan-files.php + treatPhpDocTypesAsCertain: false + dynamicConstantNames: + - DB_HOST + - DB_NAME + - DB_USER + - DB_PASSWORD + - DB_CHARSET + - DB_COLLATE + ignoreErrors: + - identifier: missingType.iterableValue + - identifier: missingType.parameter + - identifier: missingType.return diff --git a/src/DB_Command.php b/src/DB_Command.php index fe4d0f49..f7a16c2e 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -409,7 +409,7 @@ public function cli( $_, $assoc_args ) { } WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' ); - self::run( $command, $assoc_args, null, true ); + self::run( $command, $assoc_args, false, true ); } /** @@ -634,7 +634,7 @@ public function export( $args, $assoc_args ) { $result_file = $args[0]; } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- WordPress is not loaded. - $hash = substr( md5( mt_rand() ), 0, 7 ); + $hash = substr( md5( (string) mt_rand() ), 0, 7 ); $result_file = sprintf( '%s-%s-%s.sql', DB_NAME, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } @@ -709,7 +709,7 @@ public function export( $args, $assoc_args ) { } } - $escaped_command = call_user_func_array( '\WP_CLI\Utils\esc_cmd', array_merge( [ $command ], $command_esc_args ) ); + $escaped_command = Utils\esc_cmd( $command, ...$command_esc_args ); // Remove parameters not needed for SQL run. unset( $assoc_args['porcelain'] ); @@ -727,7 +727,7 @@ public function export( $args, $assoc_args ) { /** * Get the current character set of the posts table. * - * @param array Associative array of associative arguments. + * @param array $assoc_args Associative arguments. * @return string Posts table character set. */ private function get_posts_table_charset( $assoc_args ) { @@ -891,6 +891,9 @@ public function import( $args, $assoc_args ) { * Success: Exported to wordpress_dbase.sql * * @when after_wp_load + * + * @param array $args Positional arguments. + * @param array{scope?: string, network?: bool, 'all-tables-with-prefix'?: bool, 'all-tables'?: bool, format: string} $assoc_args Associative arguments. */ public function tables( $args, $assoc_args ) { @@ -1042,6 +1045,9 @@ public function tables( $args, $assoc_args ) { * 6 * * @when after_wp_load + * + * @param array $args Positional arguments. Unused. + * @param array{size_format?: string, tables?: bool, 'human-readable'?: bool, format?: string, scope?: string, network?: bool, decimals?: string, 'all-tables-with-prefix'?: bool, 'all-tables'?: bool, order: string, orderby: string} $assoc_args Associative arguments. */ public function size( $args, $assoc_args ) { global $wpdb; @@ -1114,6 +1120,8 @@ public function size( $args, $assoc_args ) { ]; } + $size_format_display = ''; + if ( ! empty( $size_format ) || $human_readable ) { foreach ( $rows as $index => $row ) { // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Backfilling WP native constants. @@ -1132,7 +1140,7 @@ public function size( $args, $assoc_args ) { // phpcs:enable if ( $human_readable ) { - $size_key = floor( log( $row['Size'] ) / log( 1000 ) ); + $size_key = floor( log( (float) $row['Size'] ) / log( 1000 ) ); $sizes = [ 'B', 'KB', 'MB', 'GB', 'TB' ]; $size_format = isset( $sizes[ $size_key ] ) ? $sizes[ $size_key ] : $sizes[0]; @@ -1184,7 +1192,7 @@ public function size( $args, $assoc_args ) { } $size_format_display = preg_replace( '/IB$/u', 'iB', strtoupper( $size_format ) ); - $decimals = Utils\get_flag_value( $assoc_args, 'decimals', 0 ); + $decimals = (int) Utils\get_flag_value( $assoc_args, 'decimals', 0 ); $rows[ $index ]['Size'] = round( (int) $row['Bytes'] / $divisor, $decimals ) . ' ' . $size_format_display; } } @@ -1203,7 +1211,7 @@ function ( $a, $b ) use ( $order, $orderby ) { list( $first, $second ) = $orderby_array; if ( 'size' === $orderby ) { - return $first['Bytes'] > $second['Bytes']; + return $first['Bytes'] <=> $second['Bytes']; } return strcmp( $first['Name'], $second['Name'] ); @@ -1428,6 +1436,10 @@ public function search( $args, $assoc_args ) { $after_context = Utils\get_flag_value( $assoc_args, 'after_context', 40 ); $after_context = '' === $after_context ? $after_context : (int) $after_context; + $default_regex_delimiter = false; + $regex_flags = false; + $regex_delimiter = ''; + $regex = Utils\get_flag_value( $assoc_args, 'regex', false ); if ( false !== $regex ) { $regex_flags = Utils\get_flag_value( $assoc_args, 'regex-flags', false ); @@ -1481,7 +1493,7 @@ public function search( $args, $assoc_args ) { $esc_like_search = '%' . Utils\esc_like( $search ) . '%'; } - $encoding = null; + $encoding = false; if ( 0 === strpos( $wpdb->charset, self::ENCODING_UTF8 ) ) { $encoding = 'UTF-8'; } @@ -1561,7 +1573,7 @@ public function search( $args, $assoc_args ) { } if ( $after_context ) { $end_offset = $offset + strlen( $match ); - $after = \cli\safe_substr( substr( $col_val, $end_offset ), 0, $after_context, false /*is_width*/, $col_encoding ); + $after = (string) \cli\safe_substr( substr( $col_val, $end_offset ), 0, $after_context, false /*is_width*/, $col_encoding ); // To lessen context duplication in output, shorten the after context if it overlaps with the next match. if ( $i + 1 < $match_cnt && $end_offset + strlen( $after ) > $matches[0][ $i + 1 ][1] ) { $after = substr( $after, 0, $matches[0][ $i + 1 ][1] - $end_offset ); @@ -1857,7 +1869,7 @@ private static function get_dbuser_dbpass_args( $assoc_args ) { * Gets the column names of a db table differentiated into key columns and text columns and all columns. * * @param string $table The table name. - * @return array A 3 element array consisting of an array of primary key column names, an array of text column names, and an array containing all column names. + * @return array{0: string[], 1: string[], 2: string[]} A 3 element array consisting of an array of primary key column names, an array of text column names, and an array containing all column names. */ private static function get_columns( $table ) { global $wpdb; @@ -1890,7 +1902,7 @@ private static function get_columns( $table ) { /** * Determines whether a column is considered text or not. * - * @param string Column type. + * @param string $type Column type. * @return bool True if text column, false otherwise. */ private static function is_text_col( $type ) { @@ -1909,6 +1921,8 @@ private static function is_text_col( $type ) { * * @param string|array $idents A single identifier or an array of identifiers. * @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings. + * + * @phpstan-return ($idents is string ? string : array) */ private static function esc_sql_ident( $idents ) { $backtick = static function ( $v ) { @@ -2155,11 +2169,6 @@ protected function get_current_sql_modes( $assoc_args ) { // Make sure the provided arguments don't interfere with the expected // output here. $args = []; - foreach ( [] as $arg ) { - if ( isset( $assoc_args[ $arg ] ) ) { - $args[ $arg ] = $assoc_args[ $arg ]; - } - } if ( null === $modes ) { $modes = []; @@ -2183,17 +2192,14 @@ protected function get_current_sql_modes( $assoc_args ) { } if ( ! empty( $stdout ) ) { + $lines = preg_split( "/\r\n|\n|\r|,/", $stdout ); $modes = array_filter( array_map( 'trim', - preg_split( "/\r\n|\n|\r|,/", $stdout ) + $lines ? $lines : [] ) ); } - - if ( false === $modes ) { - $modes = []; - } } return $modes; diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php new file mode 100644 index 00000000..80950306 --- /dev/null +++ b/tests/phpstan/scan-files.php @@ -0,0 +1,10 @@ +