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 @@
+