From 1cfdce4c08ebda96bcbc1fc9b2c6ab188c78ddfc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 19:11:05 +0000 Subject: [PATCH 1/4] Handle the wp-compat findings from PHPStan The johnbillion/wp-compat extension that comes with wp-cli-tests checks every WordPress symbol against the WordPress 4.9 baseline that wp-cli-tests configures, and only recognizes function_exists() and method_exists() guards. It reported seven errors, six of which are false positives. `WP_Plugin_Dependencies` is only reachable behind `wp_version_compare( '6.5' )` checks, in `Plugin_Command::install()` for `wp plugin install --with-dependencies` and in the command method itself for `wp plugin install-dependencies`, and WordPress 5.3 merely formalized the already documented `...$arg` parameter of `do_action()`. Ignore those per file and per error identifier, with the reason documented. The seventh is real. `wp_date()` was introduced in WordPress 5.3, but `get_wporg_data()` calls it with no guard at all, so `wp plugin list` fataled on WordPress 4.9 - 5.2 whenever a plugin had a `pubDate` on wordpress.org. Fall back to `date_i18n()`, which predates it and formats the same way, rather than failing the whole listing over one field. Co-authored-by: Pascal Birchler --- phpstan.neon.dist | 19 +++++++++++++++++++ src/Plugin_Command.php | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3b8aeab5..d71d57a8 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -14,3 +14,22 @@ parameters: - identifier: missingType.property - identifier: missingType.parameter - identifier: missingType.return + + # johnbillion/wp-compat checks every WordPress symbol against the WordPress 4.9 + # baseline that wp-cli-tests configures. It only recognizes function_exists() and + # method_exists() guards, so it cannot see the `wp_version_compare()` checks that + # gate the plugin dependency features, and reports those as errors. + + # `wp plugin install --with-dependencies` aborts on WordPress < 6.5 in + # `Plugin_Command::install()`, before reaching `get_plugin_dependencies()`, and + # `wp plugin install-dependencies` does the same check in the command method itself. + - + identifier: WPCompat.methodNotAvailable + path: src/Plugin_Command.php + + # WordPress 5.3 only formalized the already documented `...$arg` parameter of + # `do_action()` by adding it to the function signature. Additional arguments were + # collected through `func_get_args()` before that. + - + identifier: WPCompat.parameterNotAvailable.doaction.arg + path: src/Plugin_Command.php diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index e76b89b3..4c5b0f5b 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1122,7 +1122,12 @@ protected function get_wporg_data( $plugin_name ) { if ( false !== $xml ) { $xml_pub_date = $xml->xpath( '//pubDate' ); if ( $xml_pub_date ) { - $data['last_updated'] = wp_date( 'Y-m-d', strtotime( $xml_pub_date[0] ) ?: null ); + $pub_date = strtotime( $xml_pub_date[0] ) ?: null; + + // wp_date() was only introduced in WordPress 5.3. + $data['last_updated'] = function_exists( 'wp_date' ) + ? wp_date( 'Y-m-d', $pub_date ) + : date_i18n( 'Y-m-d', $pub_date ?? false ); } } } From ca915fb851b64c539940f47f019536b2b3d2001c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 19:17:54 +0000 Subject: [PATCH 2/4] Cover the wp_date() fallback on WordPress 4.9 The functional test matrix runs WordPress 4.9, where `wp_date()` does not exist. The existing wp.org status scenario is tagged `@require-wp-5.2`, so that path was never exercised on 4.9 and the fatal went unnoticed. Add a `@less-than-wp-5.3` scenario asserting that `wp plugin list` still renders `wporg_last_updated` there, which is what the `date_i18n()` fallback is for. Co-authored-by: Pascal Birchler --- features/plugin-list-wporg-status.feature | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/features/plugin-list-wporg-status.feature b/features/plugin-list-wporg-status.feature index 13e71122..2f324d23 100644 --- a/features/plugin-list-wporg-status.feature +++ b/features/plugin-list-wporg-status.feature @@ -110,3 +110,46 @@ Feature: Check the status of plugins on WordPress.org | wordpress-importer | active | 2025-09-26 | | no-longer-in-directory | closed | 2017-11-13 | | never-wporg | | | + + @less-than-wp-5.3 + Scenario: The wp.org last updated date is still rendered on WordPress < 5.3 + Given a WP install + And a wp-content/plugins/wporg-dated/wporg-dated.php file: + """ + + + + + Fri, 26 Sep 2025 21:07:26 GMT + + + + """ + + # wp_date() only exists since WordPress 5.3, so this falls back to date_i18n(). + When I run `wp plugin list --fields=name,wporg_last_updated` + Then STDOUT should be a table containing rows: + | name | wporg_last_updated | + | wporg-dated | 2025-09-26 | From 96e65714c37670c8bbb75f4e77ad6dceb1795256 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 21:14:21 +0000 Subject: [PATCH 3/4] Render the pre-5.3 last updated date in the site timezone `date_i18n()` before WordPress 5.3 expects a timestamp that already has the site's GMT offset added to it, and formats it with PHP's default timezone, which WordPress pins to UTC. Passing it the true Unix timestamp from `strtotime()` therefore rendered the UTC date, while `wp_date()` renders the site's local date, so the two disagreed by a day on non-UTC sites whenever the release time fell near midnight. Use `get_date_from_gmt()` for the fallback instead. It has been around since WordPress 1.2, handles both `timezone_string` and `gmt_offset`, and is documented to return the date in the site's timezone, so it matches what `wp_date()` does without the legacy timestamp contract. Set the test site to Asia/Tokyo and use a release time of 21:07 UTC, which is already the next day there, so the scenario fails if the fallback ever renders in UTC again. Co-authored-by: Pascal Birchler --- features/plugin-list-wporg-status.feature | 8 ++++++-- src/Plugin_Command.php | 15 +++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/features/plugin-list-wporg-status.feature b/features/plugin-list-wporg-status.feature index 2f324d23..1da8360f 100644 --- a/features/plugin-list-wporg-status.feature +++ b/features/plugin-list-wporg-status.feature @@ -114,6 +114,7 @@ Feature: Check the status of plugins on WordPress.org @less-than-wp-5.3 Scenario: The wp.org last updated date is still rendered on WordPress < 5.3 Given a WP install + And I run `wp option update timezone_string Asia/Tokyo` And a wp-content/plugins/wporg-dated/wporg-dated.php file: """ """ - # wp_date() only exists since WordPress 5.3, so this falls back to date_i18n(). + # wp_date() only exists since WordPress 5.3, so this goes through the + # get_date_from_gmt() fallback. The pubDate above is 21:07 UTC, which is already + # the next day in Asia/Tokyo, so this also pins that the fallback renders in the + # site timezone rather than in UTC. When I run `wp plugin list --fields=name,wporg_last_updated` Then STDOUT should be a table containing rows: | name | wporg_last_updated | - | wporg-dated | 2025-09-26 | + | wporg-dated | 2025-09-27 | diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 4c5b0f5b..9624d340 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1124,10 +1124,17 @@ protected function get_wporg_data( $plugin_name ) { if ( $xml_pub_date ) { $pub_date = strtotime( $xml_pub_date[0] ) ?: null; - // wp_date() was only introduced in WordPress 5.3. - $data['last_updated'] = function_exists( 'wp_date' ) - ? wp_date( 'Y-m-d', $pub_date ) - : date_i18n( 'Y-m-d', $pub_date ?? false ); + if ( function_exists( 'wp_date' ) ) { + $data['last_updated'] = wp_date( 'Y-m-d', $pub_date ); + } else { + // wp_date() is WordPress 5.3+. get_date_from_gmt() renders in the site + // timezone the same way, without date_i18n()'s pre-5.3 contract of + // expecting a timestamp that already has the offset added to it. + $data['last_updated'] = get_date_from_gmt( + gmdate( 'Y-m-d H:i:s', $pub_date ?? time() ), + 'Y-m-d' + ); + } } } } From d0483fc35fe0548c591590c8f8ef7ff328f688a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 06:03:52 +0000 Subject: [PATCH 4/4] Scope the method availability ignore to the intended methods The `WPCompat.methodNotAvailable` entry matched every diagnostic of that kind in `src/Plugin_Command.php` while only the two `WP_Plugin_Dependencies` calls needed ignoring, which would have hidden any future call to something newer than the WordPress 6.5 check that gates them. Match on the message as well, naming both the methods and that version. Co-authored-by: Pascal Birchler --- phpstan.neon.dist | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d71d57a8..b383c386 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -19,12 +19,17 @@ parameters: # baseline that wp-cli-tests configures. It only recognizes function_exists() and # method_exists() guards, so it cannot see the `wp_version_compare()` checks that # gate the plugin dependency features, and reports those as errors. + # + # The entries below match on the message as well as the identifier, so that a call to + # something introduced later than the version each feature is gated on still gets + # reported rather than swallowed by a file-wide ignore. # `wp plugin install --with-dependencies` aborts on WordPress < 6.5 in # `Plugin_Command::install()`, before reaching `get_plugin_dependencies()`, and # `wp plugin install-dependencies` does the same check in the command method itself. - identifier: WPCompat.methodNotAvailable + message: '#^WP_Plugin_Dependencies::(get_dependencies|initialize)\(\) is only available since WordPress version 6\.5\.0\.$#' path: src/Plugin_Command.php # WordPress 5.3 only formalized the already documented `...$arg` parameter of