Commit a9cd35b823 for wordpress.org

commit a9cd35b8235be6d9fc7173d87e4a223bcce2a8cf
Author: westonruter <westonruter@git.wordpress.org>
Date:   Fri Oct 16 23:47:49 2026 +0000

    Upgrade/Install: Add PHPStan types to the automatic updater.

    The update offers and results handled by `WP_Automatic_Updater` are now typed by update type, with a type alias for each kind of offer and result and a conditional return type for `WP_Automatic_Updater::update()`. A conditional parameter type for the offer would be preferable, but PHPStan only narrows the outermost condition of a nested conditional parameter type and resolves the others to `never`, so the offer is instead narrowed with inline tags in each branch that reads it, alongside comments to replace them once nested conditions are supported.

    Typing the results showed that several documented return types never matched the code. The `null|WP_Error` documented for `update()` since r32655 omitted both the `false` returned when an update is declined and the result passed through from the upgrader. Because `Plugin_Upgrader`, `Theme_Upgrader` and `Language_Pack_Upgrader` redeclare `$result` without a default, their `upgrade()` methods and the plugin and theme `install()` methods return `null` when the operation fails before the package is installed, which their documentation has omitted since r30758. The `$update` parameter of `Language_Pack_Upgrader::upgrade()` is also corrected to the update offer object that its callers pass.

    Variables that PHPStan reported as possibly undefined are initialized, and a redundant `$is_debug` assignment from r58139 is removed. This resolves 7 baselined errors across the `argument.type`, `return.type` and `variable.undefined` identifiers.

    Developed in https://github.com/WordPress/wordpress-develop/pull/14361.
    Follow-up to r11005, r25566, r30758, r32655, r58139, r63019, r63022, r63024.

    See #65817.

    Built from https://develop.svn.wordpress.org/trunk@64435


    git-svn-id: http://core.svn.wordpress.org/trunk@63585 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-admin/includes/class-language-pack-upgrader.php b/wp-admin/includes/class-language-pack-upgrader.php
index 733b109183..43f211bbd0 100644
--- a/wp-admin/includes/class-language-pack-upgrader.php
+++ b/wp-admin/includes/class-language-pack-upgrader.php
@@ -22,7 +22,8 @@ class Language_Pack_Upgrader extends WP_Upgrader {
 	 * Result of the language pack upgrade.
 	 *
 	 * @since 3.7.0
-	 * @var array|WP_Error $result
+	 * @var array|WP_Error|null $result
+	 * @phpstan-var non-empty-array<string, mixed>|WP_Error|null
 	 * @see WP_Upgrader::$result
 	 */
 	public $result;
@@ -127,10 +128,13 @@ class Language_Pack_Upgrader extends WP_Upgrader {
 	 *
 	 * @since 3.7.0
 	 *
-	 * @param string|false $update Optional. Whether an update offer is available. Default false.
+	 * @param object|false $update Optional. The language pack update offer, or false to upgrade all
+	 *                             available language packs. Default false.
 	 * @param array        $args   Optional. Other optional arguments, see
 	 *                             Language_Pack_Upgrader::bulk_upgrade(). Default empty array.
-	 * @return array|bool|WP_Error The result of the upgrade, or a WP_Error object instead.
+	 * @return array|bool|WP_Error|null The result of the upgrade, or a WP_Error object instead. Null if the
+	 *                                  upgrade failed before the package was installed, for example when
+	 *                                  the package could not be downloaded.
 	 */
 	public function upgrade( $update = false, $args = array() ) {
 		if ( $update ) {
diff --git a/wp-admin/includes/class-plugin-upgrader.php b/wp-admin/includes/class-plugin-upgrader.php
index 53e8f4af6b..428cc5941d 100644
--- a/wp-admin/includes/class-plugin-upgrader.php
+++ b/wp-admin/includes/class-plugin-upgrader.php
@@ -24,7 +24,8 @@ class Plugin_Upgrader extends WP_Upgrader {
 	 * Plugin upgrade result.
 	 *
 	 * @since 2.8.0
-	 * @var array|WP_Error $result
+	 * @var array|WP_Error|null $result
+	 * @phpstan-var non-empty-array<string, mixed>|WP_Error|null
 	 *
 	 * @see WP_Upgrader::$result
 	 */
@@ -113,7 +114,9 @@ class Plugin_Upgrader extends WP_Upgrader {
 	 *     @type bool $clear_update_cache Whether to clear the plugin updates cache if successful.
 	 *                                    Default true.
 	 * }
-	 * @return bool|WP_Error True if the installation was successful, false or a WP_Error otherwise.
+	 * @return bool|WP_Error|null True if the installation was successful, false or a WP_Error otherwise.
+	 *                            Null if the installation failed before the package was installed, for
+	 *                            example when the package could not be downloaded.
 	 */
 	public function install( $package, $args = array() ) {
 		$defaults    = array(
@@ -185,7 +188,9 @@ class Plugin_Upgrader extends WP_Upgrader {
 	 *     @type bool $clear_update_cache Whether to clear the plugin updates cache if successful.
 	 *                                    Default true.
 	 * }
-	 * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise.
+	 * @return bool|WP_Error|null True if the upgrade was successful, false or a WP_Error object otherwise.
+	 *                            Null if the upgrade failed before the package was installed, for example
+	 *                            when the package could not be downloaded.
 	 */
 	public function upgrade( $plugin, $args = array() ) {
 		$defaults    = array(
diff --git a/wp-admin/includes/class-theme-upgrader.php b/wp-admin/includes/class-theme-upgrader.php
index c665ca0631..7e571e17f5 100644
--- a/wp-admin/includes/class-theme-upgrader.php
+++ b/wp-admin/includes/class-theme-upgrader.php
@@ -24,7 +24,8 @@ class Theme_Upgrader extends WP_Upgrader {
 	 * Result of the theme upgrade offer.
 	 *
 	 * @since 2.8.0
-	 * @var array|WP_Error $result
+	 * @var array|WP_Error|null $result
+	 * @phpstan-var non-empty-array<string, mixed>|WP_Error|null
 	 * @see WP_Upgrader::$result
 	 */
 	public $result;
@@ -120,6 +121,8 @@ class Theme_Upgrader extends WP_Upgrader {
 	 * @param array $hook_extra
 	 * @param array $child_result
 	 * @return bool
+	 *
+	 * @phpstan-param non-empty-array<string, mixed> $child_result
 	 */
 	public function check_parent_theme_filter( $install_result, $hook_extra, $child_result ) {
 		// Check to see if we need to install a parent theme.
@@ -225,7 +228,9 @@ class Theme_Upgrader extends WP_Upgrader {
 	 *                                    Default true.
 	 * }
 	 *
-	 * @return bool|WP_Error True if the installation was successful, false or a WP_Error object otherwise.
+	 * @return bool|WP_Error|null True if the installation was successful, false or a WP_Error object otherwise.
+	 *                            Null if the installation failed before the package was installed, for
+	 *                            example when the package could not be downloaded.
 	 */
 	public function install( $package, $args = array() ) {
 		$defaults    = array(
@@ -290,7 +295,9 @@ class Theme_Upgrader extends WP_Upgrader {
 	 *     @type bool $clear_update_cache Whether to clear the update cache if successful.
 	 *                                    Default true.
 	 * }
-	 * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise.
+	 * @return bool|WP_Error|null True if the upgrade was successful, false or a WP_Error object otherwise.
+	 *                            Null if the upgrade failed before the package was installed, for example
+	 *                            when the package could not be downloaded.
 	 */
 	public function upgrade( $theme, $args = array() ) {
 		$defaults    = array(
diff --git a/wp-admin/includes/class-wp-automatic-updater.php b/wp-admin/includes/class-wp-automatic-updater.php
index aa4db87eff..0134a78429 100644
--- a/wp-admin/includes/class-wp-automatic-updater.php
+++ b/wp-admin/includes/class-wp-automatic-updater.php
@@ -12,6 +12,63 @@
  *
  * @since 3.7.0
  * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
+ *
+ * @phpstan-type Core_Update_Offer object{
+ *     current: string,
+ *     version: string,
+ *     php_version: string,
+ *     mysql_version: string,
+ *     new_files?: bool|string,
+ *     notify_email?: bool|string,
+ *     autoupdate?: bool,
+ *     disable_autoupdate?: bool,
+ * }&stdClass
+ * @phpstan-type Plugin_Update_Offer object{
+ *     slug: string,
+ *     plugin: string,
+ *     new_version: string,
+ *     current_version?: string|false,
+ *     requires_php?: string|false|null,
+ *     autoupdate?: bool,
+ *     disable_autoupdate?: bool,
+ * }&stdClass
+ * @phpstan-type Theme_Update_Offer object{
+ *     theme: string,
+ *     new_version: string,
+ *     current_version?: string|false,
+ *     requires_php?: string|false|null,
+ *     autoupdate?: bool,
+ *     disable_autoupdate?: bool,
+ * }&stdClass
+ * @phpstan-type Translation_Update_Offer object{
+ *     language: string,
+ *     autoupdate?: bool,
+ *     disable_autoupdate?: bool,
+ * }&stdClass
+ * @phpstan-type Core_Update_Result object{
+ *     item: Core_Update_Offer,
+ *     result: string|WP_Error,
+ *     name: string,
+ *     messages: string[],
+ * }&stdClass
+ * @phpstan-type Plugin_Update_Result object{
+ *     item: Plugin_Update_Offer,
+ *     result: true|WP_Error|null,
+ *     name: string,
+ *     messages: string[],
+ * }&stdClass
+ * @phpstan-type Theme_Update_Result object{
+ *     item: Theme_Update_Offer,
+ *     result: true|WP_Error|null,
+ *     name: string|false,
+ *     messages: string[],
+ * }&stdClass
+ * @phpstan-type Translation_Update_Result object{
+ *     item: Translation_Update_Offer,
+ *     result: array<mixed>|true|WP_Error|null,
+ *     name: string,
+ *     messages: string[],
+ * }&stdClass
  */
 #[AllowDynamicProperties]
 class WP_Automatic_Updater {
@@ -19,7 +76,13 @@ class WP_Automatic_Updater {
 	/**
 	 * Tracks update results during processing.
 	 *
-	 * @var array
+	 * @var array<string, object[]>
+	 * @phpstan-var array{
+	 *     core?: list<Core_Update_Result>,
+	 *     plugin?: list<Plugin_Update_Result>,
+	 *     theme?: list<Theme_Update_Result>,
+	 *     translation?: list<Translation_Update_Result>,
+	 * }
 	 */
 	protected $update_results = array();

@@ -191,6 +254,9 @@ class WP_Automatic_Updater {
 	 * @param string $context The filesystem context (a path) against which filesystem
 	 *                        access and status should be checked.
 	 * @return bool True if the item should be updated, false otherwise.
+	 *
+	 * @phpstan-param 'core'|'theme'|'plugin'|'translation' $type
+	 * @phpstan-param (Core_Update_Offer|Plugin_Update_Offer|Theme_Update_Offer|Translation_Update_Offer) $item
 	 */
 	public function should_update( $type, $item, $context ) {
 		// Used to see if WP_Filesystem is set up to allow unattended updates.
@@ -216,10 +282,17 @@ class WP_Automatic_Updater {
 			return false;
 		}

-		// Next up, is this an item we can update?
+		/*
+		 * Next up, is this an item we can update?
+		 *
+		 * The inline @phpstan-var tags narrow $item by $type. This should be replaced with a conditional
+		 * parameter type once PHPStan supports narrowing nested conditional parameter types.
+		 */
 		if ( 'core' === $type ) {
+			/** @phpstan-var Core_Update_Offer $item */
 			$update = Core_Upgrader::should_update_to_version( $item->current );
 		} elseif ( 'plugin' === $type || 'theme' === $type ) {
+			/** @phpstan-var (Plugin_Update_Offer|Theme_Update_Offer) $item */
 			$update = ! empty( $item->autoupdate );

 			if ( ! $update && wp_is_auto_update_enabled_for_type( $type ) ) {
@@ -278,6 +351,7 @@ class WP_Automatic_Updater {
 		if ( 'core' === $type ) {
 			global $wpdb;

+			/** @phpstan-var Core_Update_Offer $item */
 			$php_compat = version_compare( PHP_VERSION, $item->php_version, '>=' );
 			if ( file_exists( WP_CONTENT_DIR . '/db.php' ) && empty( $wpdb->is_mysql ) ) {
 				$mysql_compat = true;
@@ -292,6 +366,7 @@ class WP_Automatic_Updater {

 		// If updating a plugin or theme, ensure the minimum PHP version requirements are satisfied.
 		if ( in_array( $type, array( 'plugin', 'theme' ), true ) ) {
+			/** @phpstan-var (Plugin_Update_Offer|Theme_Update_Offer) $item */
 			if ( ! empty( $item->requires_php ) && version_compare( PHP_VERSION, $item->requires_php, '<' ) ) {
 				return false;
 			}
@@ -357,27 +432,50 @@ class WP_Automatic_Updater {
 	 *
 	 * @param string $type The type of update being checked: 'core', 'theme', 'plugin', 'translation'.
 	 * @param object $item The update offer.
-	 * @return null|WP_Error
+	 * @return WP_Error|bool|string|array|null False if the item should not be updated, otherwise the result
+	 *                                         from the upgrader: the new WordPress version for core, true for
+	 *                                         a successful plugin or theme update, the installation result for
+	 *                                         a translation, or a WP_Error on failure. Null for a plugin, theme,
+	 *                                         or translation if the update failed before the package was
+	 *                                         installed.
+	 *
+	 * @phpstan-param 'core'|'theme'|'plugin'|'translation' $type
+	 * @phpstan-param (Core_Update_Offer|Plugin_Update_Offer|Theme_Update_Offer|Translation_Update_Offer) $item
+	 * @phpstan-return (
+	 *     $type is 'core' ? string|false|WP_Error : (
+	 *         $type is 'translation' ? array<mixed>|bool|WP_Error|null : bool|WP_Error|null
+	 *     )
+	 * )
 	 */
 	public function update( $type, $item ) {
 		$skin = new Automatic_Upgrader_Skin();

+		/*
+		 * The inline @phpstan-var tags narrow $item by $type. This should be replaced with a conditional
+		 * parameter type once PHPStan supports narrowing nested conditional parameter types.
+		 */
+		$upgrader = null;
+		$context  = null;
 		switch ( $type ) {
 			case 'core':
+				/** @phpstan-var Core_Update_Offer $item */
 				// The Core upgrader doesn't use the Upgrader's skin during the actual main part of the upgrade, instead, firing a filter.
 				add_filter( 'update_feedback', array( $skin, 'feedback' ) );
 				$upgrader = new Core_Upgrader( $skin );
 				$context  = ABSPATH;
 				break;
 			case 'plugin':
+				/** @phpstan-var Plugin_Update_Offer $item */
 				$upgrader = new Plugin_Upgrader( $skin );
 				$context  = WP_PLUGIN_DIR; // We don't support custom Plugin directories, or updates for WPMU_PLUGIN_DIR.
 				break;
 			case 'theme':
+				/** @phpstan-var Theme_Update_Offer $item */
 				$upgrader = new Theme_Upgrader( $skin );
 				$context  = get_theme_root( $item->theme );
 				break;
 			case 'translation':
+				/** @phpstan-var Translation_Update_Offer $item */
 				$upgrader = new Language_Pack_Upgrader( $skin );
 				$context  = WP_CONTENT_DIR; // WP_LANG_DIR;
 				break;
@@ -400,15 +498,18 @@ class WP_Automatic_Updater {
 		 */
 		do_action( 'pre_auto_update', $type, $item, $context );

+		$item_name     = null;
 		$upgrader_item = $item;
 		switch ( $type ) {
 			case 'core':
+				/** @phpstan-var Core_Update_Offer $item */
 				/* translators: %s: WordPress version. */
 				$skin->feedback( __( 'Updating to WordPress %s' ), $item->version );
 				/* translators: %s: WordPress version. */
 				$item_name = sprintf( __( 'WordPress %s' ), $item->version );
 				break;
 			case 'theme':
+				/** @phpstan-var Theme_Update_Offer $item */
 				$upgrader_item = $item->theme;
 				$theme         = wp_get_theme( $upgrader_item );
 				$item_name     = $theme->get( 'Name' );
@@ -421,6 +522,7 @@ class WP_Automatic_Updater {
 				$skin->feedback( __( 'Updating theme: %s' ), $item_name );
 				break;
 			case 'plugin':
+				/** @phpstan-var Plugin_Update_Offer $item */
 				$upgrader_item = $item->plugin;
 				$plugin_data   = get_plugin_data( $context . '/' . $upgrader_item );
 				$item_name     = $plugin_data['Name'];
@@ -433,6 +535,8 @@ class WP_Automatic_Updater {
 				$skin->feedback( __( 'Updating plugin: %s' ), $item_name );
 				break;
 			case 'translation':
+				/** @phpstan-var Translation_Update_Offer $item */
+				/** @var Language_Pack_Upgrader $upgrader */
 				$language_item_name = $upgrader->get_name_for_update( $item );
 				/* translators: %s: Project name (plugin, theme, or WordPress). */
 				$item_name = sprintf( __( 'Translations for %s' ), $language_item_name );
@@ -446,8 +550,11 @@ class WP_Automatic_Updater {
 			$allow_relaxed_file_ownership = true;
 		}

-		$is_debug = WP_DEBUG && WP_DEBUG_LOG;
+		$was_active = null;
+		$is_debug   = WP_DEBUG && WP_DEBUG_LOG;
 		if ( 'plugin' === $type ) {
+			/** @phpstan-var Plugin_Update_Offer $item */
+			/** @var string $upgrader_item */
 			$was_active = is_plugin_active( $upgrader_item );
 			if ( $is_debug ) {
 				error_log( '    Upgrading plugin ' . var_export( $item->slug, true ) . '...' );
@@ -455,6 +562,7 @@ class WP_Automatic_Updater {
 		}

 		if ( 'theme' === $type && $is_debug ) {
+			/** @phpstan-var Theme_Update_Offer $item */
 			error_log( '    Upgrading theme ' . var_export( $item->theme, true ) . '...' );
 		}

@@ -530,13 +638,14 @@ class WP_Automatic_Updater {
 			}
 		}

-		$is_debug = WP_DEBUG && WP_DEBUG_LOG;
-
 		if ( 'theme' === $type && $is_debug ) {
+			/** @phpstan-var Theme_Update_Offer $item */
 			error_log( '    Theme ' . var_export( $item->theme, true ) . ' has been upgraded.' );
 		}

 		if ( 'plugin' === $type ) {
+			/** @phpstan-var Plugin_Update_Offer $item */
+			/** @var string $upgrader_item */
 			if ( $is_debug ) {
 				error_log( '    Plugin ' . var_export( $item->slug, true ) . ' has been upgraded.' );
 				if ( is_plugin_inactive( $upgrader_item ) ) {
diff --git a/wp-admin/includes/class-wp-upgrader.php b/wp-admin/includes/class-wp-upgrader.php
index ba27113ff7..9bd3802b04 100644
--- a/wp-admin/includes/class-wp-upgrader.php
+++ b/wp-admin/includes/class-wp-upgrader.php
@@ -73,11 +73,11 @@ class WP_Upgrader {
 	 * This is set by WP_Upgrader::install_package(), only when the package is installed
 	 * successfully. It will then be an array, unless a WP_Error is returned by the
 	 * {@see 'upgrader_post_install'} filter. In that case, the WP_Error will be assigned to
-	 * it.
+	 * it. Until then, it is an empty array, or null in subclasses which redeclare it.
 	 *
 	 * @since 2.8.0
 	 *
-	 * @var array|WP_Error $result {
+	 * @var array|WP_Error|null $result {
 	 *     @type string $source             The full path to the source the files were installed from.
 	 *     @type string $source_files       List of all the files in the source directory.
 	 *     @type string $destination        The full path to the installation destination folder.
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 63b1c50781..4124058043 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-64434';
+$wp_version = '7.2-alpha-64435';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.