Skip to content

Commit 6368bec

Browse files
committed
Merge branch 'main' into copilot/fix-1
2 parents 8d1e72f + cc2972b commit 6368bec

7 files changed

Lines changed: 196 additions & 32 deletions

File tree

AGENTS.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Instructions
2+
3+
This package is part of WP-CLI, the official command line interface for WordPress. For a detailed explanation of the project structure and development workflow, please refer to the main @README.md file.
4+
5+
## Best Practices for Code Contributions
6+
7+
When contributing to this package, please adhere to the following guidelines:
8+
9+
* **Follow Existing Conventions:** Before writing any code, analyze the existing codebase in this package to understand the coding style, naming conventions, and architectural patterns.
10+
* **Focus on the Package's Scope:** All changes should be relevant to the functionality of the package.
11+
* **Write Tests:** All new features and bug fixes must be accompanied by acceptance tests using Behat. You can find the existing tests in the `features/` directory. There may be PHPUnit unit tests as well in the `tests/` directory.
12+
* **Update Documentation:** If your changes affect the user-facing functionality, please update the relevant inline code documentation.
13+
14+
### Building and running
15+
16+
Before submitting any changes, it is crucial to validate them by running the full suite of static code analysis and tests. To run the full suite of checks, execute the following command: `composer test`.
17+
18+
This single command ensures that your changes meet all the quality gates of the project. While you can run the individual steps separately, it is highly recommended to use this single command to ensure a comprehensive validation.
19+
20+
### Useful Composer Commands
21+
22+
The project uses Composer to manage dependencies and run scripts. The following commands are available:
23+
24+
* `composer install`: Install dependencies.
25+
* `composer test`: Run the full test suite, including linting, code style checks, static analysis, and unit/behavior tests.
26+
* `composer lint`: Check for syntax errors.
27+
* `composer phpcs`: Check for code style violations.
28+
* `composer phpcbf`: Automatically fix code style violations.
29+
* `composer phpstan`: Run static analysis.
30+
* `composer phpunit`: Run unit tests.
31+
* `composer behat`: Run behavior-driven tests.
32+
33+
### Coding Style
34+
35+
The project follows the `WP_CLI_CS` coding standard, which is enforced by PHP_CodeSniffer. The configuration can be found in `phpcs.xml.dist`. Before submitting any code, please run `composer phpcs` to check for violations and `composer phpcbf` to automatically fix them.
36+
37+
## Documentation
38+
39+
The `README.md` file might be generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). In that case, changes need to be made against the corresponding part of the codebase.
40+
41+
### Inline Documentation
42+
43+
Only write high-value comments if at all. Avoid talking to the user through comments.
44+
45+
## Testing
46+
47+
The project has a comprehensive test suite that includes unit tests, behavior-driven tests, and static analysis.
48+
49+
* **Unit tests** are written with PHPUnit and can be found in the `tests/` directory. The configuration is in `phpunit.xml.dist`.
50+
* **Behavior-driven tests** are written with Behat and can be found in the `features/` directory. The configuration is in `behat.yml`.
51+
* **Static analysis** is performed with PHPStan.
52+
53+
All tests are run on GitHub Actions for every pull request.
54+
55+
When writing tests, aim to follow existing patterns. Key conventions include:
56+
57+
* When adding tests, first examine existing tests to understand and conform to established conventions.
58+
* For unit tests, extend the base `WP_CLI\Tests\TestCase` test class.
59+
* For Behat tests, only WP-CLI commands installed in `composer.json` can be run.
60+
61+
### Behat Steps
62+
63+
WP-CLI makes use of a Behat-based testing framework and provides a set of custom step definitions to write feature tests.
64+
65+
> **Note:** If you are expecting an error output in a test, you need to use `When I try ...` instead of `When I run ...` .
66+
67+
#### Given
68+
69+
* `Given an empty directory` - Creates an empty directory.
70+
* `Given /^an? (empty|non-existent) ([^\s]+) directory$/` - Creates or deletes a specific directory.
71+
* `Given an empty cache` - Clears the WP-CLI cache directory.
72+
* `Given /^an? ([^\s]+) (file|cache file):$/` - Creates a file with the given contents.
73+
* `Given /^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/` - Search and replace a string in a file using regex.
74+
* `Given /^that HTTP requests to (.*?) will respond with:$/` - Mock HTTP requests to a given URL.
75+
* `Given WP files` - Download WordPress files without installing.
76+
* `Given wp-config.php` - Create a wp-config.php file using `wp config create`.
77+
* `Given a database` - Creates an empty database.
78+
* `Given a WP install(ation)` - Installs WordPress.
79+
* `Given a WP install(ation) in :subdir` - Installs WordPress in a given directory.
80+
* `Given a WP install(ation) with Composer` - Installs WordPress with Composer.
81+
* `Given a WP install(ation) with Composer and a custom vendor directory :vendor_directory` - Installs WordPress with Composer and a custom vendor directory.
82+
* `Given /^a WP multisite (subdirectory|subdomain)?\s?(install|installation)$/` - Installs WordPress Multisite.
83+
* `Given these installed and active plugins:` - Installs and activates one or more plugins.
84+
* `Given a custom wp-content directory` - Configure a custom `wp-content` directory.
85+
* `Given download:` - Download multiple files into the given destinations.
86+
* `Given /^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/` - Store STDOUT or STDERR contents in a variable.
87+
* `Given /^a new Phar with (?:the same version|version "([^"]+)")$/` - Build a new WP-CLI Phar file with a given version.
88+
* `Given /^a downloaded Phar with (?:the same version|version "([^"]+)")$/` - Download a specific WP-CLI Phar version from GitHub.
89+
* `Given /^save the (.+) file ([\'].+[^\'])? as \{(\w+)\}$/` - Stores the contents of the given file in a variable.
90+
* `Given a misconfigured WP_CONTENT_DIR constant directory` - Modify wp-config.php to set `WP_CONTENT_DIR` to an empty string.
91+
* `Given a dependency on current wp-cli` - Add `wp-cli/wp-cli` as a Composer dependency.
92+
* `Given a PHP built-in web server` - Start a PHP built-in web server in the current directory.
93+
* `Given a PHP built-in web server to serve :subdir` - Start a PHP built-in web server in the given subdirectory.
94+
95+
#### When
96+
97+
* ``When /^I launch in the background `([^`]+)`$/`` - Launch a given command in the background.
98+
* ``When /^I (run|try) `([^`]+)`$/`` - Run or try a given command.
99+
* ``When /^I (run|try) `([^`]+)` from '([^\s]+)'$/`` - Run or try a given command in a subdirectory.
100+
* `When /^I (run|try) the previous command again$/` - Run or try the previous command again.
101+
102+
#### Then
103+
104+
* `Then /^the return code should( not)? be (\d+)$/` - Expect a specific exit code of the previous command.
105+
* `Then /^(STDOUT|STDERR) should( strictly)? (be|contain|not contain):$/` - Check the contents of STDOUT or STDERR.
106+
* `Then /^(STDOUT|STDERR) should be a number$/` - Expect STDOUT or STDERR to be a numeric value.
107+
* `Then /^(STDOUT|STDERR) should not be a number$/` - Expect STDOUT or STDERR to not be a numeric value.
108+
* `Then /^STDOUT should be a table containing rows:$/` - Expect STDOUT to be a table containing the given rows.
109+
* `Then /^STDOUT should end with a table containing rows:$/` - Expect STDOUT to end with a table containing the given rows.
110+
* `Then /^STDOUT should be JSON containing:$/` - Expect valid JSON output in STDOUT.
111+
* `Then /^STDOUT should be a JSON array containing:$/` - Expect valid JSON array output in STDOUT.
112+
* `Then /^STDOUT should be CSV containing:$/` - Expect STDOUT to be CSV containing certain values.
113+
* `Then /^STDOUT should be YAML containing:$/` - Expect STDOUT to be YAML containing certain content.
114+
* `Then /^(STDOUT|STDERR) should be empty$/` - Expect STDOUT or STDERR to be empty.
115+
* `Then /^(STDOUT|STDERR) should not be empty$/` - Expect STDOUT or STDERR not to be empty.
116+
* `Then /^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|<>) ([+\w.{}-]+)$/` - Expect STDOUT or STDERR to be a version string comparing to the given version.
117+
* `Then /^the (.+) (file|directory) should( strictly)? (exist|not exist|be:|contain:|not contain):$/` - Expect a certain file or directory to (not) exist or (not) contain certain contents.
118+
* `Then /^the contents of the (.+) file should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match file contents against a regex.
119+
* `Then /^(STDOUT|STDERR) should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match STDOUT or STDERR against a regex.
120+
* `Then /^an email should (be sent|not be sent)$/` - Expect an email to be sent (or not).
121+
* `Then the HTTP status code should be :code` - Expect the HTTP status code for visiting `http://localhost:8080`.

composer.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@
1212
}
1313
],
1414
"require": {
15-
"wp-cli/wp-cli": "^2.12"
15+
"wp-cli/wp-cli": "^2.13"
1616
},
1717
"require-dev": {
1818
"wp-cli/entity-command": "^1.3 || ^2",
19-
"wp-cli/wp-cli-tests": "^4"
19+
"wp-cli/wp-cli-tests": "^5"
2020
},
2121
"config": {
2222
"process-timeout": 7200,
2323
"sort-packages": true,
2424
"allow-plugins": {
2525
"dealerdirect/phpcodesniffer-composer-installer": true,
26-
"johnpbloch/wordpress-core-installer": true
26+
"johnpbloch/wordpress-core-installer": true,
27+
"phpstan/extension-installer": true
2728
},
2829
"lock": false
2930
},
@@ -67,6 +68,7 @@
6768
"behat-rerun": "rerun-behat-tests",
6869
"lint": "run-linter-tests",
6970
"phpcs": "run-phpcs-tests",
71+
"phpstan": "run-phpstan-tests",
7072
"phpcbf": "run-phpcbf-cleanup",
7173
"phpunit": "run-php-unit-tests",
7274
"prepare-tests": "install-package-tests",

features/db-size.feature

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ Feature: Display database size
2020
Given a WP install
2121

2222
When I run `wp db size --tables`
23-
Then STDOUT should contain:
24-
"""
25-
wp_posts 81920 B
26-
"""
23+
Then STDOUT should match /wp_posts\s+\d+ B/
2724

2825
But STDOUT should not contain:
2926
"""

phpcs.xml.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,6 @@
5555
<rule ref="WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound">
5656
<exclude-pattern>*/src/DB_Command\.php$</exclude-pattern>
5757
</rule>
58+
59+
<exclude-pattern>/tests/phpstan/scan-files</exclude-pattern>
5860
</ruleset>

phpstan.neon.dist

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
parameters:
2+
level: 9
3+
paths:
4+
- src
5+
- db-command.php
6+
scanDirectories:
7+
- vendor/wp-cli/wp-cli/php
8+
scanFiles:
9+
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
10+
- tests/phpstan/scan-files.php
11+
treatPhpDocTypesAsCertain: false
12+
dynamicConstantNames:
13+
- DB_HOST
14+
- DB_NAME
15+
- DB_USER
16+
- DB_PASSWORD
17+
- DB_CHARSET
18+
- DB_COLLATE
19+
ignoreErrors:
20+
- identifier: missingType.iterableValue
21+
- identifier: missingType.parameter
22+
- identifier: missingType.return

src/DB_Command.php

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ public function cli( $_, $assoc_args ) {
409409
}
410410

411411
WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' );
412-
self::run( $command, $assoc_args, null, true );
412+
self::run( $command, $assoc_args, false, true );
413413
}
414414

415415
/**
@@ -529,18 +529,17 @@ public function query( $args, $assoc_args ) {
529529
}
530530

531531
WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' );
532-
list( $stdout, $stderr, $exit_code ) = self::run( $command, $assoc_args, false );
533-
534-
if ( $exit_code ) {
535-
WP_CLI::error( "Query failed: {$stderr}" );
536-
}
537532

538533
if ( $is_row_modifying_query ) {
539-
$output_lines = explode( "\n", trim( $stdout ) );
540-
$affected_rows = (int) trim( end( $output_lines ) );
534+
list( $stdout, $stderr, $exit_code ) = self::run( $command, $assoc_args, false );
535+
$output_lines = explode( "\n", trim( $stdout ) );
536+
$affected_rows = (int) trim( end( $output_lines ) );
537+
if ( $exit_code ) {
538+
WP_CLI::error( "Query failed: {$stderr}" );
539+
}
541540
WP_CLI::success( "Query succeeded. Rows affected: {$affected_rows}" );
542-
} elseif ( ! empty( $stdout ) ) {
543-
WP_CLI::line( $stdout );
541+
} else {
542+
self::run( $command, $assoc_args );
544543
}
545544
}
546545

@@ -637,7 +636,7 @@ public function export( $args, $assoc_args ) {
637636
$result_file = $args[0];
638637
} else {
639638
// phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- WordPress is not loaded.
640-
$hash = substr( md5( mt_rand() ), 0, 7 );
639+
$hash = substr( md5( (string) mt_rand() ), 0, 7 );
641640
$result_file = sprintf( '%s-%s-%s.sql', DB_NAME, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
642641

643642
}
@@ -712,7 +711,7 @@ public function export( $args, $assoc_args ) {
712711
}
713712
}
714713

715-
$escaped_command = call_user_func_array( '\WP_CLI\Utils\esc_cmd', array_merge( [ $command ], $command_esc_args ) );
714+
$escaped_command = Utils\esc_cmd( $command, ...$command_esc_args );
716715

717716
// Remove parameters not needed for SQL run.
718717
unset( $assoc_args['porcelain'] );
@@ -730,7 +729,7 @@ public function export( $args, $assoc_args ) {
730729
/**
731730
* Get the current character set of the posts table.
732731
*
733-
* @param array Associative array of associative arguments.
732+
* @param array $assoc_args Associative arguments.
734733
* @return string Posts table character set.
735734
*/
736735
private function get_posts_table_charset( $assoc_args ) {
@@ -897,6 +896,9 @@ public function import( $args, $assoc_args ) {
897896
* Success: Exported to wordpress_dbase.sql
898897
*
899898
* @when after_wp_load
899+
*
900+
* @param array<string> $args Positional arguments.
901+
* @param array{scope?: string, network?: bool, 'all-tables-with-prefix'?: bool, 'all-tables'?: bool, format: string} $assoc_args Associative arguments.
900902
*/
901903
public function tables( $args, $assoc_args ) {
902904

@@ -1048,6 +1050,9 @@ public function tables( $args, $assoc_args ) {
10481050
* 6
10491051
*
10501052
* @when after_wp_load
1053+
*
1054+
* @param array $args Positional arguments. Unused.
1055+
* @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.
10511056
*/
10521057
public function size( $args, $assoc_args ) {
10531058
global $wpdb;
@@ -1120,6 +1125,8 @@ public function size( $args, $assoc_args ) {
11201125
];
11211126
}
11221127

1128+
$size_format_display = '';
1129+
11231130
if ( ! empty( $size_format ) || $human_readable ) {
11241131
foreach ( $rows as $index => $row ) {
11251132
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Backfilling WP native constants.
@@ -1138,7 +1145,7 @@ public function size( $args, $assoc_args ) {
11381145
// phpcs:enable
11391146

11401147
if ( $human_readable ) {
1141-
$size_key = floor( log( $row['Size'] ) / log( 1000 ) );
1148+
$size_key = floor( log( (float) $row['Size'] ) / log( 1000 ) );
11421149
$sizes = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
11431150

11441151
$size_format = isset( $sizes[ $size_key ] ) ? $sizes[ $size_key ] : $sizes[0];
@@ -1190,7 +1197,7 @@ public function size( $args, $assoc_args ) {
11901197
}
11911198
$size_format_display = preg_replace( '/IB$/u', 'iB', strtoupper( $size_format ) );
11921199

1193-
$decimals = Utils\get_flag_value( $assoc_args, 'decimals', 0 );
1200+
$decimals = (int) Utils\get_flag_value( $assoc_args, 'decimals', 0 );
11941201
$rows[ $index ]['Size'] = round( (int) $row['Bytes'] / $divisor, $decimals ) . ' ' . $size_format_display;
11951202
}
11961203
}
@@ -1209,7 +1216,7 @@ function ( $a, $b ) use ( $order, $orderby ) {
12091216
list( $first, $second ) = $orderby_array;
12101217

12111218
if ( 'size' === $orderby ) {
1212-
return $first['Bytes'] > $second['Bytes'];
1219+
return $first['Bytes'] <=> $second['Bytes'];
12131220
}
12141221

12151222
return strcmp( $first['Name'], $second['Name'] );
@@ -1434,6 +1441,10 @@ public function search( $args, $assoc_args ) {
14341441
$after_context = Utils\get_flag_value( $assoc_args, 'after_context', 40 );
14351442
$after_context = '' === $after_context ? $after_context : (int) $after_context;
14361443

1444+
$default_regex_delimiter = false;
1445+
$regex_flags = false;
1446+
$regex_delimiter = '';
1447+
14371448
$regex = Utils\get_flag_value( $assoc_args, 'regex', false );
14381449
if ( false !== $regex ) {
14391450
$regex_flags = Utils\get_flag_value( $assoc_args, 'regex-flags', false );
@@ -1487,7 +1498,7 @@ public function search( $args, $assoc_args ) {
14871498
$esc_like_search = '%' . Utils\esc_like( $search ) . '%';
14881499
}
14891500

1490-
$encoding = null;
1501+
$encoding = false;
14911502
if ( 0 === strpos( $wpdb->charset, self::ENCODING_UTF8 ) ) {
14921503
$encoding = 'UTF-8';
14931504
}
@@ -1567,7 +1578,7 @@ public function search( $args, $assoc_args ) {
15671578
}
15681579
if ( $after_context ) {
15691580
$end_offset = $offset + strlen( $match );
1570-
$after = \cli\safe_substr( substr( $col_val, $end_offset ), 0, $after_context, false /*is_width*/, $col_encoding );
1581+
$after = (string) \cli\safe_substr( substr( $col_val, $end_offset ), 0, $after_context, false /*is_width*/, $col_encoding );
15711582
// To lessen context duplication in output, shorten the after context if it overlaps with the next match.
15721583
if ( $i + 1 < $match_cnt && $end_offset + strlen( $after ) > $matches[0][ $i + 1 ][1] ) {
15731584
$after = substr( $after, 0, $matches[0][ $i + 1 ][1] - $end_offset );
@@ -1866,7 +1877,7 @@ private static function get_dbuser_dbpass_args( $assoc_args ) {
18661877
* Gets the column names of a db table differentiated into key columns and text columns and all columns.
18671878
*
18681879
* @param string $table The table name.
1869-
* @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.
1880+
* @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.
18701881
*/
18711882
private static function get_columns( $table ) {
18721883
global $wpdb;
@@ -1899,7 +1910,7 @@ private static function get_columns( $table ) {
18991910
/**
19001911
* Determines whether a column is considered text or not.
19011912
*
1902-
* @param string Column type.
1913+
* @param string $type Column type.
19031914
* @return bool True if text column, false otherwise.
19041915
*/
19051916
private static function is_text_col( $type ) {
@@ -1918,6 +1929,8 @@ private static function is_text_col( $type ) {
19181929
*
19191930
* @param string|array $idents A single identifier or an array of identifiers.
19201931
* @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings.
1932+
*
1933+
* @phpstan-return ($idents is string ? string : array)
19211934
*/
19221935
private static function esc_sql_ident( $idents ) {
19231936
$backtick = static function ( $v ) {
@@ -2188,17 +2201,14 @@ protected function get_current_sql_modes( $assoc_args ) {
21882201
}
21892202

21902203
if ( ! empty( $stdout ) ) {
2204+
$lines = preg_split( "/\r\n|\n|\r|,/", $stdout );
21912205
$modes = array_filter(
21922206
array_map(
21932207
'trim',
2194-
preg_split( "/\r\n|\n|\r|,/", $stdout )
2208+
$lines ? $lines : []
21952209
)
21962210
);
21972211
}
2198-
2199-
if ( false === $modes ) {
2200-
$modes = [];
2201-
}
22022212
}
22032213

22042214
return $modes;

0 commit comments

Comments
 (0)