diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 738e7d1721d2e..cbaa19f3e8db1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -53,7 +53,6 @@ includes: - tests/phpstan/baselines/property.notFound.neon - tests/phpstan/baselines/property.private.neon - tests/phpstan/baselines/return.type.neon - - tests/phpstan/baselines/return.unusedType.neon - tests/phpstan/baselines/varTag.noVariable.neon - tests/phpstan/baselines/variable.undefined.neon # phpstan:baselines end @@ -132,6 +131,18 @@ parameters: path: src/wp-includes/compat.php reportUnmatched: false + # Level 4: + # wp_imagecreatetruecolor() returns whatever imagecreatetruecolor() returns: a resource on PHP 7.4 and a + # GdImage instance on PHP 8.0+. Its docblock lists both, which is correct for WP's 7.4+ range. Because + # `phpVersion.min` is 70400, PHPStan reads the function from its pre-8.0 map, sees only a resource, and + # reports GdImage as unused. Removing GdImage would make the docblock wrong on every PHP 8 install. + # `reportUnmatched: false` so this entry lapses quietly if PHPStan stops reporting it. + - + message: '#^Function wp_imagecreatetruecolor\(\) never returns GdImage so it can be removed from the return type\.$#' + identifier: return.unusedType + path: src/wp-includes/media.php + reportUnmatched: false + # Level 5: # substr_compare()'s $length has always accepted null, meaning "compare the full length". PHP 7.4 spelled # that `int $length = null`, where the null default makes the parameter implicitly nullable, and PHP 8.0 diff --git a/src/wp-admin/includes/plugin-install.php b/src/wp-admin/includes/plugin-install.php index ffae71c5b81f6..496b7ffd36679 100644 --- a/src/wp-admin/includes/plugin-install.php +++ b/src/wp-admin/includes/plugin-install.php @@ -223,9 +223,9 @@ function plugins_api( $action, $args = array() ) { * * @since 2.7.0 * - * @param object|WP_Error $res Response object or WP_Error. - * @param string $action The type of information being requested from the Plugin Installation API. - * @param object $args Plugin API arguments. + * @param object|array|WP_Error $res Response object or array, or WP_Error on failure. + * @param string $action The type of information being requested from the Plugin Installation API. + * @param object $args Plugin API arguments. */ return apply_filters( 'plugins_api_result', $res, $action, $args ); } diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index b7ef9ec69601b..f08f81766df67 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -1163,7 +1163,7 @@ function update_meta( $meta_id, $meta_key, $meta_value ) { * @access private * * @param int|WP_Post $post Post ID or post object. - * @return void|int|WP_Error Void if nothing fixed. 0 or WP_Error on update failure. The post ID on update success. + * @return void|int Void if nothing fixed. 0 on update failure. The post ID on update success. */ function _fix_attachment_links( $post ) { $post = get_post( $post, ARRAY_A ); diff --git a/src/wp-admin/includes/update.php b/src/wp-admin/includes/update.php index 22f44a33dbb54..e7b02a2a62c8a 100644 --- a/src/wp-admin/includes/update.php +++ b/src/wp-admin/includes/update.php @@ -11,7 +11,7 @@ * * @since 2.7.0 * - * @return object|array|false The response from the API on success, false on failure. + * @return object|false The response from the API on success, false on failure. */ function get_preferred_from_update_core() { $updates = get_core_updates(); diff --git a/src/wp-includes/category-template.php b/src/wp-includes/category-template.php index f48ebe6adb3d7..130f55e3fe043 100644 --- a/src/wp-includes/category-template.php +++ b/src/wp-includes/category-template.php @@ -1224,11 +1224,12 @@ function get_the_tag_list( $before = '', $sep = '', $after = '', $post_id = 0 ) * * @since 2.3.0 * - * @param string $tag_list List of tags. - * @param string $before String to use before the tags. - * @param string $sep String to use between the tags. - * @param string $after String to use after the tags. - * @param int $post_id Post ID. + * @param string|false|WP_Error $tag_list A list of tags on success, false if there are no terms, + * WP_Error on failure. + * @param string $before String to use before the tags. + * @param string $sep String to use between the tags. + * @param string $after String to use after the tags. + * @param int $post_id Post ID. */ return apply_filters( 'the_tags', $tag_list, $before, $sep, $after, $post_id ); } diff --git a/src/wp-includes/category.php b/src/wp-includes/category.php index e6085418e255d..e93c6c122eb75 100644 --- a/src/wp-includes/category.php +++ b/src/wp-includes/category.php @@ -428,7 +428,7 @@ function clean_category_cache( $id ) { * @since 4.4.0 The `$category` parameter now also accepts a WP_Term object. * @access private * - * @param array|object|WP_Term $category Category row object or array. + * @param array|object|WP_Term|null $category Category row object or array. Null is left unchanged. */ function _make_cat_compat( &$category ) { if ( is_object( $category ) && ! is_wp_error( $category ) ) { diff --git a/src/wp-includes/class-wp-recovery-mode-cookie-service.php b/src/wp-includes/class-wp-recovery-mode-cookie-service.php index a2ee34a723aa8..feefc942b6d52 100644 --- a/src/wp-includes/class-wp-recovery-mode-cookie-service.php +++ b/src/wp-includes/class-wp-recovery-mode-cookie-service.php @@ -195,7 +195,7 @@ private function generate_cookie() { * @since 5.2.0 * * @param string $data Data to hash. - * @return string|false The hashed $data, or false on failure. + * @return string The hashed $data. */ private function recovery_mode_hash( $data ) { $default_keys = array_unique( diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 33447382297a2..4c6945a62648d 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -4760,9 +4760,9 @@ function wp_get_code_editor_settings( $args ) { * * @since 4.9.0 * - * @param array $settings The array of settings passed to the code editor. - * A falsey value disables the editor. - * @param array $args { + * @param array|false $settings The array of settings passed to the code editor. + * A falsey value disables the editor. + * @param array $args { * Args passed when calling `get_code_editor_settings()`. * * @type string $type The MIME type of the file to be edited. diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 4cd69ade6cbb7..3e84e97f40689 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5587,7 +5587,11 @@ function get_post_galleries( $post, $html = true ) { * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @param bool $html Optional. Whether to return HTML or data. Default is true. - * @return string|array Gallery data and srcs parsed from the expanded shortcode. + * @return string|array|false Gallery HTML or an array of gallery data and srcs parsed from + * the expanded shortcode, depending on `$html`. False if the post has + * no gallery. + * + * @phpstan-return ( $html is false ? array|false : string|false ) */ function get_post_gallery( $post = 0, $html = true ) { $galleries = get_post_galleries( $post, $html ); @@ -5598,9 +5602,10 @@ function get_post_gallery( $post = 0, $html = true ) { * * @since 3.6.0 * - * @param array $gallery The first-found post gallery. - * @param int|WP_Post $post Post ID or object. - * @param array $galleries Associative array of all found post galleries. + * @param string|array|false $gallery The first-found post gallery, or false if the post has no gallery. + * An HTML string when `$html` is true, otherwise an array. + * @param int|WP_Post $post Post ID or object. + * @param array $galleries Associative array of all found post galleries. */ return apply_filters( 'get_post_gallery', $gallery, $post, $galleries ); } diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index d73d945ef839a..e0afc05cd1b54 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -3895,7 +3895,7 @@ function get_post_mime_types() { * @param string|string[] $wildcard_mime_types Mime types, e.g. `audio/mpeg`, `image` (same as `image/*`), * or `flash` (same as `*flash*`). * @param string|string[] $real_mime_types Real post mime type values. - * @return array array(wildcard=>array(real types)). + * @return array array(wildcard=>array(real types)). */ function wp_match_mime_types( $wildcard_mime_types, $real_mime_types ) { $matches = array(); @@ -7637,11 +7637,15 @@ function wp_attachment_is_image( $post = null ) { * * @param string|int $mime MIME type or attachment ID. * @param string $preferred_ext File format to prefer in return. Default '.png'. - * @return string|false Icon, false otherwise. + * @return string|false Icon URL, or false if no icon was found. */ function wp_mime_type_icon( $mime = 0, $preferred_ext = '.png' ) { + $icon = false; if ( ! is_numeric( $mime ) ) { - $icon = wp_cache_get( "mime_type_icon_$mime" ); + $cached_icon = wp_cache_get( "mime_type_icon_$mime" ); + if ( is_string( $cached_icon ) ) { + $icon = $cached_icon; + } } // Check if preferred file format variable is present and is a validly formatted file extension. @@ -7756,11 +7760,12 @@ function wp_mime_type_icon( $mime = 0, $preferred_ext = '.png' ) { foreach ( $matches as $match => $wilds ) { foreach ( $wilds as $wild ) { - if ( ! isset( $types[ $wild ] ) ) { + $type_icon = $types[ $wild ] ?? null; + if ( ! is_string( $type_icon ) ) { continue; } - $icon = $types[ $wild ]; + $icon = $type_icon; if ( ! is_numeric( $mime ) ) { wp_cache_add( "mime_type_icon_$mime", $icon ); } @@ -7774,10 +7779,10 @@ function wp_mime_type_icon( $mime = 0, $preferred_ext = '.png' ) { * * @since 2.1.0 * - * @param string $icon Path to the mime type icon. - * @param string $mime Mime type. - * @param int $post_id Attachment ID. Will equal 0 if the function passed - * the mime type. + * @param string|false $icon URL of the mime type icon, or false if no icon was found. + * @param string|int $mime Mime type, or 0 if no post was found for the passed ID. + * @param int $post_id Attachment ID. Will equal 0 if the function passed + * the mime type or no post was found for the passed ID. */ return apply_filters( 'wp_mime_type_icon', $icon, $mime, $post_id ); } diff --git a/src/wp-includes/revision.php b/src/wp-includes/revision.php index 77b0b6425ecb4..03fb126029f77 100644 --- a/src/wp-includes/revision.php +++ b/src/wp-includes/revision.php @@ -870,13 +870,9 @@ function wp_revisions_to_keep( $post ) { * @access private * * @param WP_Post $post - * @return WP_Post|false + * @return WP_Post */ function _set_preview( $post ) { - if ( ! is_object( $post ) ) { - return $post; - } - $preview = wp_get_post_autosave( $post->ID ); if ( is_object( $preview ) ) { diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 12a5db2b792ec..2bf9f30dfba16 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -1273,7 +1273,10 @@ function get_term_field( $field, $term, $taxonomy = '', $context = 'display' ) { * * @param int|object $id Term ID or object. * @param string $taxonomy Taxonomy name. - * @return string|int|null|WP_Error Will return empty string if $term is not an object. + * @return WP_Term|string|WP_Error The term sanitized for editing, WP_Error if the term ID is empty or the + * taxonomy does not exist, or an empty string if the term does not exist. + * + * @phpstan-return WP_Term|''|WP_Error */ function get_term_to_edit( $id, $taxonomy ) { $term = get_term( $id, $taxonomy ); @@ -1786,12 +1789,18 @@ function term_is_ancestor_of( $term1, $term2, $taxonomy ) { * * @since 2.3.0 * - * @param array|object $term The term to check. - * @param string $taxonomy The taxonomy name to use. - * @param string $context Optional. Context in which to sanitize the term. - * Accepts 'raw', 'edit', 'db', 'display', 'rss', - * 'attribute', or 'js'. Default 'display'. - * @return array|object Term with all fields sanitized. + * @param array|object $term The term to check. + * @param string $taxonomy The taxonomy name to use. + * @param string $context Optional. Context in which to sanitize the term. + * Accepts 'raw', 'edit', 'db', 'display', 'rss', + * 'attribute', or 'js'. Default 'display'. + * @return array|object Term with all fields sanitized (will be the same type as `$term`). + * + * @phpstan-return ( + * $term is WP_Term ? WP_Term : ( + * $term is array ? array : object + * ) + * ) */ function sanitize_term( $term, $taxonomy, $context = 'display' ) { $fields = array( 'term_id', 'name', 'description', 'slug', 'count', 'parent', 'term_group', 'term_taxonomy_id', 'object_id' ); @@ -1836,16 +1845,16 @@ function sanitize_term( $term, $taxonomy, $context = 'display' ) { * * @since 2.3.0 * - * @param string $field Term field to sanitize. - * @param string $value Search for this term value. - * @param int $term_id Term ID. - * @param string $taxonomy Taxonomy name. - * @param string $context Context in which to sanitize the term field. - * Accepts 'raw', 'edit', 'db', 'display', 'rss', - * 'attribute', or 'js'. + * @param string $field Term field to sanitize. + * @param string|int $value Search for this term value. + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy name. + * @param string $context Context in which to sanitize the term field. + * Accepts 'raw', 'edit', 'db', 'display', 'rss', + * 'attribute', or 'js'. * @return mixed Sanitized field. * - * @phpstan-template T of string + * @phpstan-template T of string|int * @phpstan-param T $value * @phpstan-return ( * $field is 'parent'|'term_id'|'count'|'term_group'|'term_taxonomy_id'|'object_id' diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 02a412b291a8e..518278feb9ed5 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -5341,8 +5341,9 @@ function wp_is_password_reset_allowed_for_user( $user ) { * * @since 2.7.0 * - * @param bool $allow Whether to allow the password to be reset. Default true. - * @param int $user_id The ID of the user attempting to reset a password. + * @param bool|WP_Error $allow Whether to allow the password to be reset, or a WP_Error to explain + * why it is not allowed. Default true. + * @param int $user_id The ID of the user attempting to reset a password. */ return apply_filters( 'allow_password_reset', $allow, $user->ID ); } diff --git a/src/wp-signup.php b/src/wp-signup.php index 4eb8c60b21408..dd4e5d5365f74 100644 --- a/src/wp-signup.php +++ b/src/wp-signup.php @@ -443,8 +443,8 @@ function signup_another_blog( $blogname = '', $blog_title = '', $errors = '' ) { * @global string $domain The new site's domain. * @global string $path The new site's path. * - * @return null|bool True if site signup was validated, false on error. - * The function halts all execution if the user is not logged in. + * @return bool True if site signup was validated, false on error. + * The function halts all execution if the user is not logged in. */ function validate_another_blog_signup() { global $blogname, $blog_title, $errors, $domain, $path; diff --git a/tests/phpstan/baselines/property.nonObject.neon b/tests/phpstan/baselines/property.nonObject.neon index 895c9e3656d3d..e17cdfa2c2458 100644 --- a/tests/phpstan/baselines/property.nonObject.neon +++ b/tests/phpstan/baselines/property.nonObject.neon @@ -48,21 +48,6 @@ parameters: identifier: property.nonObject count: 1 path: ../../../src/wp-admin/includes/class-theme-upgrader.php - - - message: '#^Cannot access property \$current on array\|object\.$#' - identifier: property.nonObject - count: 3 - path: ../../../src/wp-admin/includes/class-wp-automatic-updater.php - - - message: '#^Cannot access property \$response on array\|object\.$#' - identifier: property.nonObject - count: 1 - path: ../../../src/wp-admin/includes/class-wp-automatic-updater.php - - - message: '#^Cannot access property \$version on array\|object\.$#' - identifier: property.nonObject - count: 1 - path: ../../../src/wp-admin/includes/class-wp-automatic-updater.php - message: '#^Cannot access property \$info on array\|object\.$#' identifier: property.nonObject @@ -73,16 +58,6 @@ parameters: identifier: property.nonObject count: 1 path: ../../../src/wp-admin/includes/class-wp-plugin-install-list-table.php - - - message: '#^Cannot access property \$parent on array\|object\.$#' - identifier: property.nonObject - count: 1 - path: ../../../src/wp-admin/includes/class-wp-terms-list-table.php - - - message: '#^Cannot access property \$term_id on array\|object\.$#' - identifier: property.nonObject - count: 2 - path: ../../../src/wp-admin/includes/class-wp-terms-list-table.php - message: '#^Cannot access property \$info on array\|object\.$#' identifier: property.nonObject diff --git a/tests/phpstan/baselines/return.unusedType.neon b/tests/phpstan/baselines/return.unusedType.neon deleted file mode 100644 index 2acebf529a781..0000000000000 --- a/tests/phpstan/baselines/return.unusedType.neon +++ /dev/null @@ -1,100 +0,0 @@ -# PHPStan baseline for the `return.unusedType` errors in WordPress core. -# -# https://phpstan.org/error-identifiers/return.unusedType -# -# Each entry is scoped to a single file and carries an exact occurrence count, -# so that a new instance is reported as a new error rather than being absorbed -# silently. Fixing an occurrence therefore means decrementing or removing its -# entry here as part of the same change. -# -# The goal is to empty this file and delete it, along with the `includes` entry -# for it in phpstan.neon.dist. -# -# Generated by `composer phpstan:baselines`. Do not edit by hand; regenerate with -# -# composer phpstan:baselines -- --identifier=return.unusedType -# -# which reruns the analysis with this file suppressed so the errors surface again. - -parameters: - ignoreErrors: - - - message: '#^Function plugins_api\(\) never returns array so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-admin/includes/plugin-install.php - - - message: '#^Function _fix_attachment_links\(\) never returns WP_Error so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-admin/includes/post.php - - - message: '#^Function get_preferred_from_update_core\(\) never returns array so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-admin/includes/update.php - - - message: '#^Function get_the_tag_list\(\) never returns WP_Error so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/category-template.php - - - message: '#^Function get_the_tag_list\(\) never returns false so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/category-template.php - - - message: '#^Function get_category\(\) never returns null so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/category.php - - - message: '#^Method WP_Recovery_Mode_Cookie_Service\:\:recovery_mode_hash\(\) never returns false so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/class-wp-recovery-mode-cookie-service.php - - - message: '#^Function wp_get_code_editor_settings\(\) never returns false so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/general-template.php - - - message: '#^Function get_post_gallery\(\) never returns string so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/media.php - - - message: '#^Function wp_imagecreatetruecolor\(\) never returns GdImage so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/media.php - - - message: '#^Function wp_mime_type_icon\(\) never returns false so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/post.php - - - message: '#^Function _set_preview\(\) never returns false so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/revision.php - - - message: '#^Function get_term_to_edit\(\) never returns int so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/taxonomy.php - - - message: '#^Function get_term_to_edit\(\) never returns null so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/taxonomy.php - - - message: '#^Function wp_is_password_reset_allowed_for_user\(\) never returns WP_Error so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-includes/user.php - - - message: '#^Function validate_another_blog_signup\(\) never returns null so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: ../../../src/wp-signup.php diff --git a/tests/phpunit/tests/post/attachments.php b/tests/phpunit/tests/post/attachments.php index 76315dcd7c954..979ff4025bcf4 100644 --- a/tests/phpunit/tests/post/attachments.php +++ b/tests/phpunit/tests/post/attachments.php @@ -552,4 +552,73 @@ public function test_wp_mime_type_icon_video_with_preferred_ext() { $this->assertStringContainsString( 'images/media/video.png', $icon1, 'Mime type icon should be correctly returned with ".png" argument.' ); $this->assertStringContainsString( 'images/media/video.png', $icon2, 'Mime type icon should be correctly returned with "png" argument.' ); } + + /** + * Tests that false is passed to the filter and returned when no icon matches. + * + * @ticket 65817 + * + * @dataProvider data_wp_mime_type_icon_without_matching_icon + * + * @param int|string $mime MIME type or attachment ID. + */ + public function test_wp_mime_type_icon_without_matching_icon( $mime ) { + // With no icon files available, not even the default icon can match. + wp_cache_set( 'icon_files', array() ); + + $filtered_icons = array(); + add_filter( + 'wp_mime_type_icon', + static function ( $icon ) use ( &$filtered_icons ) { + $filtered_icons[] = $icon; + return $icon; + } + ); + + $this->assertFalse( wp_mime_type_icon( $mime ), 'Expected false to be returned.' ); + $this->assertSame( array( false ), $filtered_icons, 'Expected false to be passed to the filter.' ); + } + + /** + * Data provider for test_wp_mime_type_icon_without_matching_icon(). + * + * @return array + */ + public static function data_wp_mime_type_icon_without_matching_icon(): array { + return array( + 'nonexistent attachment ID' => array( PHP_INT_MAX ), + 'unknown MIME type' => array( 'foo/bar' ), + ); + } + + /** + * Tests that a cached icon that is not a string is ignored and the icon is looked up again. + * + * @ticket 65817 + * + * @dataProvider data_wp_mime_type_icon_ignores_non_string_cached_icon + * + * @param mixed $cached_icon Value stored in the icon cache for the MIME type. + */ + public function test_wp_mime_type_icon_ignores_non_string_cached_icon( $cached_icon ) { + wp_cache_set( 'mime_type_icon_video/mp4', $cached_icon ); + + $icon = wp_mime_type_icon( 'video/mp4' ); + + $this->assertIsString( $icon, 'Expected the icon to be looked up again rather than taken from the cache.' ); + $this->assertStringContainsString( 'images/media/video.png', $icon ); + } + + /** + * Data provider for test_wp_mime_type_icon_ignores_non_string_cached_icon(). + * + * @return array + */ + public static function data_wp_mime_type_icon_ignores_non_string_cached_icon(): array { + return array( + 'true' => array( true ), + 'array' => array( array( 'http://example.com/icon.png' ) ), + 'int' => array( 1 ), + ); + } }