From b99e672903099cbccb81295b753a27d87a2cd35d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:45:05 +0000 Subject: [PATCH 01/24] General: Add the PHPStan types the stubs carry for the general-purpose functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. This moves the entries for `functions.php` and `load.php` in, along with `wpdb::prepare()`, which takes a `literal-string`, and `WP_Object_Cache::delete()`, whose second parameter is deprecated. Three entries are adjusted rather than taken as they are. `size_format()` is stated in terms of what core's own parameter allows, `get_tag_regex()` does return an empty string for an empty tag, and neither `wp_is_uuid()` nor the `$version` it validates is as narrow as the map has it: the function calls `_doing_it_wrong()`, so it is not pure. `wp_upload_bits()` documents its second parameter as deprecated and unused, and the one call in core that passes it an empty string rather than null is corrected. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-admin/includes/image.php | 2 +- src/wp-includes/class-wp-object-cache.php | 2 + src/wp-includes/class-wpdb.php | 4 ++ src/wp-includes/functions.php | 72 +++++++++++++++++++++ src/wp-includes/load.php | 30 +++++++++ tests/phpstan/baselines/argument.type.neon | 4 +- tests/phpstan/baselines/if.alwaysFalse.neon | 5 -- 7 files changed, 111 insertions(+), 8 deletions(-) diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 935c613d561e9..c3a4a8d769be0 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -635,7 +635,7 @@ function wp_generate_attachment_metadata( $attachment_id, $file ) { break; } $basename = str_replace( '.', '-', wp_basename( $file ) ) . '-image' . $ext; - $uploaded = wp_upload_bits( $basename, '', $metadata['image']['data'] ); + $uploaded = wp_upload_bits( $basename, null, $metadata['image']['data'] ); if ( false === $uploaded['error'] ) { $image_attachment = array( 'post_mime_type' => $metadata['image']['mime'], diff --git a/src/wp-includes/class-wp-object-cache.php b/src/wp-includes/class-wp-object-cache.php index cda63e66d49ef..d79246ad2e3b0 100644 --- a/src/wp-includes/class-wp-object-cache.php +++ b/src/wp-includes/class-wp-object-cache.php @@ -420,6 +420,8 @@ public function get_multiple( $keys, $group = 'default', $force = false ) { * @param string $group Optional. Where the cache contents are grouped. Default 'default'. * @param bool $deprecated Optional. Unused. Default false. * @return bool True on success, false if the contents were not deleted. + * + * @phpstan-param false $deprecated */ public function delete( $key, $group = 'default', $deprecated = false ) { if ( ! $this->is_valid_key( $key ) ) { diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index 869111efd0797..4ce20eb61ee0a 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -1465,6 +1465,8 @@ private function _escape_identifier_value( $identifier ) { * @param mixed ...$args Further variables to substitute into the query's placeholders * if being called with individual arguments. * @return string|null Sanitized query string, if there is a query to prepare. + * + * @phpstan-param literal-string $query */ public function prepare( $query, ...$args ) { if ( is_null( $query ) ) { @@ -3098,6 +3100,8 @@ public function get_var( $query = null, $x = 0, $y = 0 ) { * ) * : null * ) + * + * @phpstan-param int<0, max> $y */ public function get_row( $query = null, $output = OBJECT, $y = 0 ) { $this->func_call = "\$db->get_row(\"$query\",$output,$y)"; diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 4f5916a322620..3577d9609db29 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -31,6 +31,8 @@ * @param bool $translate Whether the return date should be translated. Default true. * @return string|int|false Integer if `$format` is 'U' or 'G', string otherwise. * False on failure. + * + * @phpstan-return ($format is 'G'|'U' ? int|false : string|false) */ function mysql2date( $format, $date, $translate = true ) { if ( empty( $date ) ) { @@ -74,6 +76,8 @@ function mysql2date( $format, $date, $translate = true ) { * or PHP date format string (e.g. 'Y-m-d'). * @param bool $gmt Optional. Whether to use GMT timezone. Default false. * @return int|string Integer if `$type` is 'timestamp' or 'U', string otherwise. + * + * @phpstan-return ($type is 'timestamp'|'U' ? int : string) */ function current_time( $type, $gmt = false ) { // Don't use non-GMT timestamp, unless you know the difference and really need to. @@ -465,6 +469,7 @@ function number_format_i18n( $number, $decimals = 0 ) { * @return string|false Number string on success, false on failure. * * @phpstan-param int|float|numeric-string $bytes + * @phpstan-return ( $bytes is int<0, max> ? string : string|false ) */ function size_format( $bytes, $decimals = 0 ) { if ( ! is_numeric( $bytes ) ) { @@ -632,6 +637,10 @@ function get_weekstartend( $mysqlstring, $start_of_week = '' ) { * * @param string|array|object $data Data that might be serialized. * @return mixed A scalar data. + * + * @phpstan-template T of mixed + * @phpstan-param T $data + * @phpstan-return (T is array|object|string ? string : T) */ function maybe_serialize( $data ) { if ( is_array( $data ) || is_object( $data ) ) { @@ -840,6 +849,8 @@ function xmlrpc_removepostdata( $content ) { * * @param string $content Content to extract URLs from. * @return string[] Array of URLs found in passed string. + * + * @phpstan-return ($content is empty ? array{} : list) */ function wp_extract_urls( $content ) { preg_match_all( @@ -993,6 +1004,8 @@ function do_enclose( $content, $post ) { * @param string $url URL to retrieve HTTP headers from. * @param bool $deprecated Not Used. * @return \WpOrg\Requests\Utility\CaseInsensitiveDictionary|false Headers on success, false on failure. + * + * @phpstan-param false $deprecated */ function wp_get_http_headers( $url, $deprecated = false ) { if ( ! empty( $deprecated ) ) { @@ -1022,6 +1035,8 @@ function wp_get_http_headers( $url, $deprecated = false ) { * @global string $previousday The day of the previous post in the loop. * * @return int 1 when new day, 0 if not a new day. + * + * @phpstan-return 0|1 */ function is_new_day() { global $currentday, $previousday; @@ -1600,6 +1615,9 @@ function get_num_queries() { * * @param string $yn Character string containing either 'y' (yes) or 'n' (no). * @return bool True if 'y', false on anything else. + * + * @phpstan-pure + * @phpstan-return ($yn is 'y' ? true : false) */ function bool_from_yn( $yn ) { return ( 'y' === strtolower( $yn ) ); @@ -1879,6 +1897,8 @@ function is_blog_installed() { * @param int|string $action Optional. Nonce action name. Default -1. * @param string $name Optional. Nonce name. Default '_wpnonce'. * @return string Escaped URL with nonce action added. + * + * @phpstan-param -1|string $action */ function wp_nonce_url( $actionurl, $action = -1, $name = '_wpnonce' ) { $actionurl = str_replace( '&', '&', $actionurl ); @@ -1910,6 +1930,8 @@ function wp_nonce_url( $actionurl, $action = -1, $name = '_wpnonce' ) { * @param bool $referer Optional. Whether to set the referer field for validation. Default true. * @param bool $display Optional. Whether to display or return hidden form field. Default true. * @return string Nonce field HTML markup. + * + * @phpstan-param -1|string $action */ function wp_nonce_field( $action = -1, $name = '_wpnonce', $referer = true, $display = true ) { $name = esc_attr( $name ); @@ -2131,6 +2153,9 @@ function wp_mkdir_p( $target ) { * * @param string $path File path. * @return bool True if path is absolute, false is not absolute. + * + * @phpstan-assert-if-true =non-falsy-string $path + * @phpstan-return ($path is non-falsy-string ? bool : false) */ function path_is_absolute( $path ) { /* @@ -2173,6 +2198,8 @@ function path_is_absolute( $path ) { * @param string $base Base path. * @param string $path Path relative to $base. * @return string The path with the base or absolute path. + * + * @phpstan-return non-falsy-string */ function path_join( $base, $path ) { if ( path_is_absolute( $path ) ) { @@ -2939,6 +2966,9 @@ function _wp_check_existing_file_names( $filename, $files ) { * } * @phpstan-return array{ file: non-empty-string, url: non-empty-string, type: string|false, error: false } * |array{ error: string, ... } + * + * @phpstan-param non-empty-string $name + * @phpstan-param null $deprecated */ function wp_upload_bits( $name, $deprecated, $bits, $time = null ) { if ( ! empty( $deprecated ) ) { @@ -4459,6 +4489,9 @@ function _wp_die_process_input( $message, $title = '', $args = array() ) { * @param int $depth Optional. Maximum depth to walk through $value. Must be * greater than 0. Default 512. * @return string|false The JSON encoded string, or false if it cannot be encoded. + * + * @phpstan-param int<1, max> $depth + * @phpstan-return non-empty-string|false */ function wp_json_encode( $value, $flags = 0, $depth = 512 ) { $json = json_encode( $value, $flags, $depth ); @@ -5416,6 +5449,8 @@ function _wp_to_kebab_case( $input_string ) { * @return bool Whether the variable is a list. * * @phpstan-assert-if-true array $data + * @phpstan-pure + * @phpstan-return ($data is array ? true : false) */ function wp_is_numeric_array( $data ): bool { if ( ! is_array( $data ) ) { @@ -6227,6 +6262,8 @@ function _doing_it_wrong( $function_name, $message, $version ) { * before passing to this function to avoid being stripped {@see wp_kses()}. * @param int $error_level Optional. The designated error type for this error. * Only works with E_USER family of constants. Default E_USER_NOTICE. + * + * @phpstan-param \E_USER_ERROR|\E_USER_WARNING|\E_USER_NOTICE|\E_USER_DEPRECATED $error_level */ function wp_trigger_error( $function_name, $message, $error_level = E_USER_NOTICE ) { /** @@ -6414,6 +6451,8 @@ function iis7_supports_permalinks() { * @param string $file File path. * @param string[] $allowed_files Optional. Array of allowed files. Default empty array. * @return int 0 means nothing is wrong, greater than 0 means something was wrong. + * + * @phpstan-return ($file is '' ? 0 : ($allowed_files is empty ? 0|1|2 : 0|1|2|3)) */ function validate_file( $file, $allowed_files = array() ) { if ( ! is_scalar( $file ) || '' === $file ) { @@ -7170,6 +7209,8 @@ function __return_false() { // phpcs:ignore WordPress.NamingConventions.ValidFun * @since 3.0.0 * * @return int 0. + * + * @phpstan-return 0 */ function __return_zero() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore return 0; @@ -7183,6 +7224,8 @@ function __return_zero() { // phpcs:ignore WordPress.NamingConventions.ValidFunc * @since 3.0.0 * * @return array Empty array. + * + * @phpstan-return array{} */ function __return_empty_array() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore return array(); @@ -7211,6 +7254,8 @@ function __return_null() { // phpcs:ignore WordPress.NamingConventions.ValidFunc * @see __return_null() * * @return string Empty string. + * + * @phpstan-return '' */ function __return_empty_string() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore return ''; @@ -7424,6 +7469,8 @@ function wp_allowed_protocols() { * the raw array returned. Default true. * @return string|array Either a string containing a reversed comma separated trace or an array * of individual calls. + * + * @phpstan-return ($pretty is true ? string : list) */ function wp_debug_backtrace_summary( $ignore_class = null, $skip_frames = 0, $pretty = true ) { static $truncate_paths; @@ -7556,6 +7603,8 @@ function _device_can_upload() { * * @param string $path The resource path or URL. * @return bool True if the path is a stream URL. + * + * @phpstan-assert-if-true =non-falsy-string $path */ function wp_is_stream( $path ) { $scheme_separator = strpos( $path, '://' ); @@ -7733,6 +7782,8 @@ function wp_auth_check( $response ) { * * @param string $tag An HTML tag name. Example: 'video'. * @return string Tag RegEx. + * + * @phpstan-return ( $tag is ''|'0' ? '' : non-falsy-string ) */ function get_tag_regex( $tag ) { if ( empty( $tag ) ) { @@ -8142,6 +8193,9 @@ function wp_raise_memory_limit( $context = 'admin' ) { * @since 7.0.0 Uses wp_rand if available. * * @return string UUID. + * + * @phpstan-impure + * @phpstan-return lowercase-string&non-falsy-string */ function wp_generate_uuid4() { static $backup_randomizer = false; @@ -8179,6 +8233,11 @@ function wp_generate_uuid4() { * @param int $version Specify which version of UUID to check against. Default is none, * to accept any UUID version. Otherwise, only version allowed is `4`. * @return bool The string is a valid UUID or false on failure. + * + * @phpstan-template TUuid of string + * @phpstan-param TUuid $uuid + * @phpstan-assert-if-true =TUuid&lowercase-string&non-falsy-string $uuid + * @phpstan-return ($version is 4|null ? bool : false) */ function wp_is_uuid( $uuid, $version = null ) { @@ -8211,6 +8270,9 @@ function wp_is_uuid( $uuid, $version = null ) { * * @param string $prefix Prefix for the returned ID. * @return string Unique ID. + * + * @phpstan-impure + * @phpstan-return ($prefix is ''|numeric-string ? numeric-string : string)&non-falsy-string&($prefix is lowercase-string ? lowercase-string : string) */ function wp_unique_id( $prefix = '' ) { static $id_counter = 0; @@ -8230,6 +8292,9 @@ function wp_unique_id( $prefix = '' ) { * * @param string $prefix Optional. Prefix for the returned ID. Default empty string. * @return string Incremental ID per prefix. + * + * @phpstan-impure + * @phpstan-return ($prefix is ''|numeric-string ? numeric-string : string)&non-falsy-string&($prefix is lowercase-string ? lowercase-string : string) */ function wp_unique_prefixed_id( $prefix = '' ) { static $id_counters = array(); @@ -8263,6 +8328,9 @@ function wp_unique_prefixed_id( $prefix = '' ) { * @param array $data The input array to generate an ID from. * @param string $prefix Optional. A prefix to prepend to the generated ID. Default empty string. * @return string The generated unique ID for the array. + * + * @phpstan-param non-empty-array $data + * @phpstan-return ($prefix is lowercase-string ? lowercase-string&non-falsy-string : non-falsy-string) */ function wp_unique_id_from_values( array $data, string $prefix = '' ): string { if ( empty( $data ) ) { @@ -9160,6 +9228,8 @@ function clean_dirsize_cache( $path ) { * @since 6.7.0 * * @return string The current WordPress version. + * + * @phpstan-return non-falsy-string */ function wp_get_wp_version() { static $wp_version; @@ -9427,6 +9497,8 @@ function wp_is_heic_image_mime_type( $mime_type ) { * * @param string $message The message to hash. * @return string The hash of the message. + * + * @phpstan-return non-falsy-string */ function wp_fast_hash( #[\SensitiveParameter] diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 9d407453424c5..1846b04573110 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -11,6 +11,8 @@ * @since 4.4.0 * * @return string The HTTP protocol. Default: HTTP/1.0. + * + * @phpstan-return 'HTTP/1.0'|'HTTP/1.1'|'HTTP/2'|'HTTP/2.0'|'HTTP/3' */ function wp_get_server_protocol() { $protocol = $_SERVER['SERVER_PROTOCOL'] ?? ''; @@ -1464,6 +1466,31 @@ function is_multisite() { * * @param mixed $maybeint Data you wish to have converted to a non-negative integer. * @return int A non-negative integer. + * + * @phpstan-template T of int + * @phpstan-param T|scalar|array|resource|null $maybeint + * @phpstan-pure + * @phpstan-return ( + * $maybeint is T&int<0, max> + * ? T + * : ( + * $maybeint is int + * ? int<1, max> + * : ( + * $maybeint is empty + * ? 0 + * : ( + * $maybeint is numeric-string + * ? int<0, max> + * : ( + * $maybeint is string + * ? 0 + * : ($maybeint is true|non-empty-array ? 1 : ($maybeint is bool ? 0|1 : int<0, max>)) + * ) + * ) + * ) + * ) + * ) */ function absint( $maybeint ) { return abs( (int) $maybeint ); @@ -1477,6 +1504,8 @@ function absint( $maybeint ) { * @global int $blog_id * * @return int Site ID. + * + * @phpstan-return int<0, max> */ function get_current_blog_id() { global $blog_id; @@ -1800,6 +1829,7 @@ function wp_doing_cron() { * @return bool Whether the variable is an instance of WP_Error. * * @phpstan-assert-if-true WP_Error $thing + * @phpstan-return ($thing is WP_Error ? true : false) */ function is_wp_error( $thing ) { $is_wp_error = ( $thing instanceof WP_Error ); diff --git a/tests/phpstan/baselines/argument.type.neon b/tests/phpstan/baselines/argument.type.neon index 14681f13d929a..4c0a9652d0e98 100644 --- a/tests/phpstan/baselines/argument.type.neon +++ b/tests/phpstan/baselines/argument.type.neon @@ -729,7 +729,7 @@ parameters: count: 1 path: ../../../src/wp-includes/class-wp-customize-manager.php - - message: '#^Parameter \#1 \$postarr of function wp_insert_post expects array\{ID\?\: int, post_author\?\: int, post_date\?\: string, post_date_gmt\?\: string, post_content\?\: string, post_content_filtered\?\: string, post_title\?\: string, post_excerpt\?\: string, \.\.\., \.\.\.\}, array\ given\.$#' + message: '#^Parameter \#1 \$postarr of function wp_insert_post expects array\{ID\?\: int, post_author\?\: int, post_date\?\: string, post_date_gmt\?\: string, post_content\?\: string, post_content_filtered\?\: string, post_title\?\: string, post_excerpt\?\: string, \.\.\., \.\.\.\}, array\\|int\<1, max\>\|string\|false\> given\.$#' identifier: argument.type count: 1 path: ../../../src/wp-includes/class-wp-customize-manager.php @@ -849,7 +849,7 @@ parameters: count: 1 path: ../../../src/wp-includes/class-wp-widget.php - - message: '#^Parameter \#1 \$postarr of function wp_insert_post expects array\{ID\?\: int, post_author\?\: int, post_date\?\: string, post_date_gmt\?\: string, post_content\?\: string, post_content_filtered\?\: string, post_title\?\: string, post_excerpt\?\: string, \.\.\., \.\.\.\}, array\{post_author\: int, post_date\: int\|string, post_date_gmt\: int\|string, post_content\: string, post_title\: string, post_category\: array\\|string, post_status\: ''draft''\|''publish''\} given\.$#' + message: '#^Parameter \#1 \$postarr of function wp_insert_post expects array\{ID\?\: int, post_author\?\: int, post_date\?\: string, post_date_gmt\?\: string, post_content\?\: string, post_content_filtered\?\: string, post_title\?\: string, post_excerpt\?\: string, \.\.\., \.\.\.\}, array\{post_author\: int, post_date\: string, post_date_gmt\: string, post_content\: string, post_title\: string, post_category\: array\\|string, post_status\: ''draft''\|''publish''\} given\.$#' identifier: argument.type count: 1 path: ../../../src/wp-includes/class-wp-xmlrpc-server.php diff --git a/tests/phpstan/baselines/if.alwaysFalse.neon b/tests/phpstan/baselines/if.alwaysFalse.neon index 6e33ed2a5365c..cc72813ceafbc 100644 --- a/tests/phpstan/baselines/if.alwaysFalse.neon +++ b/tests/phpstan/baselines/if.alwaysFalse.neon @@ -28,11 +28,6 @@ parameters: identifier: if.alwaysFalse count: 2 path: ../../../src/wp-includes/class-wp-block-processor.php - - - message: '#^If condition is always false\.$#' - identifier: if.alwaysFalse - count: 1 - path: ../../../src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php - message: '#^If condition is always false\.$#' identifier: if.alwaysFalse From fc80e10a4bd61c97b673fdf462000c15ddea022e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:45:43 +0000 Subject: [PATCH 02/24] Formatting: Add the PHPStan types the stubs carry for the formatting functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. Among them the conditional returns of `addslashes_gpc()` and `zeroise()`, the template that keeps `urlencode_deep()` and `sanitize_category()` returning what they were given, and `trailingslashit()` returning a `non-falsy-string`, which is what the extension szepeviktor/phpstan-wordpress carries for it does and all it does. The `@phpstan-param-out` the map writes for `wp_parse_str()` is left out: PHPStan reports core's own assignment to `$result` as not matching it, and the narrower type is not worth an error inside the function it describes. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-includes/formatting.php | 49 ++++++++++++++++++++++++++++++++++ src/wp-includes/kses.php | 2 ++ 2 files changed, 51 insertions(+) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index abbf18c3b74d2..b1d3534cfc20b 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -611,6 +611,8 @@ function wpautop( $text, $br = true ) { * * @param string $input The text which has to be formatted. * @return string[] Array of the formatted text. + * + * @phpstan-return non-empty-list */ function wp_html_split( $input ) { return preg_split( get_html_split_regex(), $input, -1, PREG_SPLIT_DELIM_CAPTURE ); @@ -622,6 +624,8 @@ function wp_html_split( $input ) { * @since 4.4.0 * * @return string The regular expression. + * + * @phpstan-return non-falsy-string */ function get_html_split_regex() { static $regex; @@ -2278,6 +2282,9 @@ function sanitize_title_for_query( $title ) { * When set to 'save', additional entities are converted to hyphens * or stripped entirely. Default 'display'. * @return string The sanitized title. + * + * @phpstan-param 'display'|'save' $context + * @phpstan-return lowercase-string */ function sanitize_title_with_dashes( $title, $raw_title = '', $context = 'display' ) { $title = strip_tags( $title ); @@ -2412,6 +2419,10 @@ function sanitize_title_with_dashes( $title, $raw_title = '', $context = 'displa * * @param string $orderby Order by clause to be validated. * @return string|false Returns $orderby if valid, false otherwise. + * + * @phpstan-template T of string + * @phpstan-param T $orderby + * @phpstan-return (T is non-falsy-string ? T|false : false) */ function sanitize_sql_orderby( $orderby ) { if ( preg_match( '/^\s*(([a-z0-9_]+|`[a-z0-9_]+`)(\s+(ASC|DESC))?\s*(,\s*(?=[a-z0-9_`])|$))+$/i', $orderby ) || preg_match( '/^\s*RAND\(\s*\)\s*$/i', $orderby ) ) { @@ -2488,6 +2499,8 @@ function sanitize_locale_name( $locale_name ) { * @param string $content String of characters to be converted. * @param string $deprecated Not used. * @return string Converted string. + * + * @phpstan-param '' $deprecated */ function convert_chars( $content, $deprecated = '' ) { if ( ! empty( $deprecated ) ) { @@ -2788,6 +2801,17 @@ function format_to_edit( $content, $rich_text = false ) { * @param int $number Number to append zeros to if not greater than threshold. * @param int $threshold Digit places number needs to be to not have zeros added. * @return string Adds leading zeros to number if needed. + * + * @phpstan-param int<0, max> $threshold + * @phpstan-return ( + * $threshold is 0 + * ? lowercase-string&non-empty-string&numeric-string + * : ( + * $number is int<0, max> + * ? lowercase-string&non-empty-string&numeric-string + * : lowercase-string&non-empty-string + * ) + * ) */ function zeroise( $number, $threshold ) { return sprintf( '%0' . $threshold . 's', $number ); @@ -2800,6 +2824,8 @@ function zeroise( $number, $threshold ) { * * @param string $value Value to which backslashes will be added. * @return string String with backslashes inserted. + * + * @phpstan-pure */ function backslashit( $value ) { if ( isset( $value[0] ) && $value[0] >= '0' && $value[0] <= '9' ) { @@ -2821,6 +2847,9 @@ function backslashit( $value ) { * * @param string $value Value to which trailing slash will be added. * @return string String with trailing slash added. + * + * @phpstan-pure + * @phpstan-return non-falsy-string */ function trailingslashit( $value ) { return untrailingslashit( $value ) . '/'; @@ -2836,6 +2865,8 @@ function trailingslashit( $value ) { * * @param string $value Value from which trailing slashes will be removed. * @return string String without the trailing slashes. + * + * @phpstan-pure */ function untrailingslashit( $value ) { return rtrim( $value, '/\\' ); @@ -2884,6 +2915,10 @@ function stripslashes_from_strings_only( $value ) { * * @param mixed $value The array or string to be encoded. * @return mixed The encoded value. + * + * @phpstan-template T + * @phpstan-param T $value + * @phpstan-return T */ function urlencode_deep( $value ) { return map_deep( $value, 'urlencode' ); @@ -2896,6 +2931,10 @@ function urlencode_deep( $value ) { * * @param mixed $value The array or string to be encoded. * @return mixed The encoded value. + * + * @phpstan-template T + * @phpstan-param T $value + * @phpstan-return T */ function rawurlencode_deep( $value ) { return map_deep( $value, 'rawurlencode' ); @@ -2908,6 +2947,10 @@ function rawurlencode_deep( $value ) { * * @param mixed $value The array or string to be decoded. * @return mixed The decoded value. + * + * @phpstan-template T + * @phpstan-param T $value + * @phpstan-return T */ function urldecode_deep( $value ) { return map_deep( $value, 'urldecode' ); @@ -2948,6 +2991,8 @@ function urldecode_deep( $value ) { * @param string $email_address Email address. * @param int $hex_encoding Optional. Set to 1 to enable hex encoding. * @return string Converted email address. + * + * @phpstan-param 0|1 $hex_encoding */ function antispambot( $email_address, $hex_encoding = 0 ) { $obfuscated = ''; @@ -3609,6 +3654,8 @@ function convert_smilies( $text ) { * @param string $email Email address to verify. * @param bool $deprecated Deprecated. * @return string|false Valid email address on success, false on failure. + * + * @phpstan-param false $deprecated */ function is_email( $email, $deprecated = false ) { if ( ! empty( $deprecated ) ) { @@ -5870,6 +5917,8 @@ function sanitize_trackback_urls( $to_ping ) { * T is array ? array, ( value-of is string ? string : value-of )> : T * ) * ) + * + * @phpstan-pure */ function wp_slash( $value ) { if ( is_array( $value ) ) { diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 9394b75989912..ed04ad1878ad1 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2643,6 +2643,8 @@ function kses_init() { * @param string $css A string of CSS rules, decoded from an HTML `style` attribute. * @param string $deprecated Not used. * @return string Filtered string of CSS rules, needing HTML escaping before sending back to a `style` attribute. + * + * @phpstan-param '' $deprecated */ function safecss_filter_attr( $css, $deprecated = '' ) { if ( ! empty( $deprecated ) ) { From 340580458025f670c6db28d02766775158e0561a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:46:23 +0000 Subject: [PATCH 03/24] I18N: Add the PHPStan types the stubs carry for the localization functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `wp_get_word_count_type()` and `WP_Locale::$word_count_type` are one of three known values, `load_plugin_textdomain()` takes a deprecated second parameter, and `WP_Translations::translate()` returns null only when it is given null. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-includes/class-wp-locale.php | 4 ++++ src/wp-includes/l10n.php | 4 ++++ src/wp-includes/l10n/class-wp-translations.php | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/src/wp-includes/class-wp-locale.php b/src/wp-includes/class-wp-locale.php index e18c0c4f1d897..37a2abf5cec53 100644 --- a/src/wp-includes/class-wp-locale.php +++ b/src/wp-includes/class-wp-locale.php @@ -119,6 +119,8 @@ class WP_Locale { * * @since 6.2.0 * @var string + * + * @phpstan-var 'characters_excluding_spaces'|'characters_including_spaces'|'words' */ public $word_count_type; @@ -442,6 +444,8 @@ public function get_list_item_separator() { * * @return string Localized word count type. Possible values are `characters_excluding_spaces`, * `characters_including_spaces`, or `words`. Defaults to `words`. + * + * @phpstan-return 'characters_excluding_spaces'|'characters_including_spaces'|'words' */ public function get_word_count_type() { diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index 7b9d2652d41dd..95c41fb3f6bdc 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -995,6 +995,8 @@ function load_default_textdomain( $locale = null ) { * @param string|false $plugin_rel_path Optional. Relative path to WP_PLUGIN_DIR where the .mo file resides. * Default false. * @return bool True when textdomain is successfully loaded, false otherwise. + * + * @phpstan-param false $deprecated */ function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) { /** @var WP_Textdomain_Registry $wp_textdomain_registry */ @@ -2066,6 +2068,8 @@ function wp_get_list_item_separator() { * * @return string Locale-specific word count type. Possible values are `characters_excluding_spaces`, * `characters_including_spaces`, or `words`. Defaults to `words`. + * + * @phpstan-return 'characters_excluding_spaces'|'characters_including_spaces'|'words' */ function wp_get_word_count_type() { global $wp_locale; diff --git a/src/wp-includes/l10n/class-wp-translations.php b/src/wp-includes/l10n/class-wp-translations.php index e919fea8b94b3..b158501b64e83 100644 --- a/src/wp-includes/l10n/class-wp-translations.php +++ b/src/wp-includes/l10n/class-wp-translations.php @@ -112,6 +112,11 @@ private function make_entry( $original, $translations ): Translation_Entry { * @param int|float $count Count. Should be an integer, but some plugins pass floats. * @param string|null $context Context. * @return string|null Translation if it exists, or the unchanged singular string. + * + * @phpstan-template T of string|null + * @phpstan-param T $singular + * @phpstan-param int $count + * @phpstan-return ($singular is null ? null : ($plural is null ? T : string)) */ public function translate_plural( $singular, $plural, $count = 1, $context = '' ) { if ( null === $singular || null === $plural ) { @@ -135,6 +140,8 @@ public function translate_plural( $singular, $plural, $count = 1, $context = '' * @param string|null $singular Singular string. * @param string|null $context Context. * @return string|null Translation if it exists, or the unchanged singular string + * + * @phpstan-return ($singular is null ? null : string) */ public function translate( $singular, $context = '' ) { if ( null === $singular ) { From 3be1e63a7eabe3369f6fdbd4d2778f22b5f34929 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:47:51 +0000 Subject: [PATCH 04/24] Plugins: Add the PHPStan types the stubs carry for the plugin and hook API. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. A hook name is a `non-empty-string`, `has_filter()` and `has_action()` return a bool only when asked about the hook rather than a callback, `did_action()` and `did_filter()` count from zero, and the callbacks `register_activation_hook()` and the menu page functions accept are described as the callables they are. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-admin/includes/plugin-install.php | 2 + src/wp-admin/includes/plugin.php | 33 ++++++++++ src/wp-includes/plugin.php | 30 +++++++++ .../baselines/parameter.defaultValue.neon | 65 ------------------- 4 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/wp-admin/includes/plugin-install.php b/src/wp-admin/includes/plugin-install.php index b37b956d3455c..ffae71c5b81f6 100644 --- a/src/wp-admin/includes/plugin-install.php +++ b/src/wp-admin/includes/plugin-install.php @@ -309,6 +309,8 @@ function install_dashboard() { * @since 4.6.0 The `$type_selector` parameter was deprecated. * * @param bool $deprecated Not used. + * + * @phpstan-param true $deprecated */ function install_search_form( $deprecated = true ) { $type = isset( $_REQUEST['type'] ) ? wp_unslash( $_REQUEST['type'] ) : 'term'; diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 9969316ab8d58..29a67dd8d5d0f 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -900,6 +900,9 @@ function activate_plugins( $plugins, $redirect = '', $network_wide = false, $sil * @param string $deprecated Not used. * @return bool|null|WP_Error True on success, false if `$plugins` is empty, `WP_Error` on failure. * `null` if filesystem credentials are required to proceed. + * + * @phpstan-param '' $deprecated + * @phpstan-return ($plugins is empty ? false : true|null|WP_Error) */ function delete_plugins( $plugins, $deprecated = '' ) { global $wp_filesystem; @@ -1105,6 +1108,8 @@ function validate_active_plugins() { * * @param string $plugin Path to the plugin file relative to the plugins directory. * @return int|WP_Error 0 on success, WP_Error on failure. + * + * @phpstan-return ($plugin is empty ? WP_Error : 0|WP_Error) */ function validate_plugin( $plugin ) { if ( validate_file( $plugin ) ) { @@ -1388,6 +1393,8 @@ function uninstall_plugin( $plugin ) { * * Pass 'none' to leave div.wp-menu-image empty so an icon can be added via CSS. * @param int|float $position Optional. The position in the menu order this item should appear. * @return string The resulting page's hook_suffix. + * + * @phpstan-param ''|callable $callback */ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $icon_url = '', $position = null ) { global $menu, $admin_page_hooks, $_registered_pages, $_parent_pages; @@ -1483,6 +1490,8 @@ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $call * @param callable $callback Optional. The function to be called to output the content for this page. * @param int|float $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { global $submenu, $menu, $_wp_real_parent_file, $_wp_submenu_nopriv, @@ -1596,6 +1605,8 @@ function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_management_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'tools.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1620,6 +1631,8 @@ function add_management_page( $page_title, $menu_title, $capability, $menu_slug, * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_options_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'options-general.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1644,6 +1657,8 @@ function add_options_page( $page_title, $menu_title, $capability, $menu_slug, $c * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_theme_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'themes.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1668,6 +1683,8 @@ function add_theme_page( $page_title, $menu_title, $capability, $menu_slug, $cal * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_plugins_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'plugins.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1692,6 +1709,8 @@ function add_plugins_page( $page_title, $menu_title, $capability, $menu_slug, $c * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_users_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { if ( current_user_can( 'edit_users' ) ) { @@ -1721,6 +1740,8 @@ function add_users_page( $page_title, $menu_title, $capability, $menu_slug, $cal * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_dashboard_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'index.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1745,6 +1766,8 @@ function add_dashboard_page( $page_title, $menu_title, $capability, $menu_slug, * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_posts_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'edit.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1769,6 +1792,8 @@ function add_posts_page( $page_title, $menu_title, $capability, $menu_slug, $cal * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_media_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'upload.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1793,6 +1818,8 @@ function add_media_page( $page_title, $menu_title, $capability, $menu_slug, $cal * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_links_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'link-manager.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1817,6 +1844,8 @@ function add_links_page( $page_title, $menu_title, $capability, $menu_slug, $cal * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_pages_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'edit.php?post_type=page', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -1841,6 +1870,8 @@ function add_pages_page( $page_title, $menu_title, $capability, $menu_slug, $cal * @param callable $callback Optional. The function to be called to output the content for this page. * @param int $position Optional. The position in the menu order this item should appear. * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required. + * + * @phpstan-param ''|callable $callback */ function add_comments_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { return add_submenu_page( 'edit-comments.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position ); @@ -2136,6 +2167,8 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * @param string $parent_page The slug name for the parent menu (or the file name of a standard * WordPress admin page). * @return string Hook name for the plugin page. + * + * @phpstan-return non-falsy-string */ function get_plugin_page_hookname( $plugin_page, $parent_page ) { global $admin_page_hooks; diff --git a/src/wp-includes/plugin.php b/src/wp-includes/plugin.php index 38e88aa96bb00..10e40626265a7 100644 --- a/src/wp-includes/plugin.php +++ b/src/wp-includes/plugin.php @@ -171,6 +171,8 @@ function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) * @param mixed ...$args Optional. Additional parameters to pass to the callback functions. * @no-named-arguments * @return mixed The filtered value after all hooked functions are applied to it. + * + * @phpstan-param non-empty-string $hook_name */ function apply_filters( $hook_name, $value, ...$args ) { global $wp_filter, $wp_filters, $wp_current_filter; @@ -226,6 +228,8 @@ function apply_filters( $hook_name, $value, ...$args ) { * @param string $hook_name The name of the filter hook. * @param non-empty-list $args The arguments supplied to the functions hooked to `$hook_name`. * @return mixed The filtered value after all hooked functions are applied to it. + * + * @phpstan-param non-empty-string $hook_name */ function apply_filters_ref_array( $hook_name, $args ) { global $wp_filter, $wp_filters, $wp_current_filter; @@ -285,6 +289,7 @@ function apply_filters_ref_array( $hook_name, $args ) { * If `$callback` and `$priority` are both provided, a boolean is returned * for whether the specific function is registered at that priority. * @phpstan-param Maybe_Callable|false $callback + * @phpstan-return ($callback is false ? bool : false|int) */ function has_filter( $hook_name, $callback = false, $priority = false ) { global $wp_filter; @@ -369,6 +374,8 @@ function remove_all_filters( $hook_name, $priority = false ) { * @global string[] $wp_current_filter Stores the list of current filters with the current one last * * @return string|false Hook name of the current filter, false if no filter is running. + * + * @phpstan-return non-empty-string|false */ function current_filter() { global $wp_current_filter; @@ -416,6 +423,8 @@ function doing_filter( $hook_name = null ) { * * @param string $hook_name The name of the filter hook. * @return int The number of times the filter hook has been applied. + * + * @phpstan-return int<0, max> */ function did_filter( $hook_name ) { global $wp_filters; @@ -488,6 +497,8 @@ function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) * @param mixed ...$arg Optional. Additional arguments which are passed on to the * functions hooked to the action. Default empty. * @no-named-arguments + * + * @phpstan-param non-empty-string $hook_name */ function do_action( $hook_name, ...$arg ) { global $wp_filter, $wp_actions, $wp_current_filter; @@ -543,6 +554,8 @@ function do_action( $hook_name, ...$arg ) { * * @param string $hook_name The name of the action to be executed. * @param list $args The arguments supplied to the functions hooked to `$hook_name`. + * + * @phpstan-param non-empty-string $hook_name */ function do_action_ref_array( $hook_name, $args ) { global $wp_filter, $wp_actions, $wp_current_filter; @@ -600,6 +613,7 @@ function do_action_ref_array( $hook_name, $args ) { * If `$callback` and `$priority` are both provided, a boolean is returned * for whether the specific function is registered at that priority. * @phpstan-param Maybe_Callable|false $callback + * @phpstan-return ($callback is false ? bool : false|int) */ function has_action( $hook_name, $callback = false, $priority = false ) { return has_filter( $hook_name, $callback, $priority ); @@ -650,6 +664,8 @@ function remove_all_actions( $hook_name, $priority = false ) { * @since 3.9.0 * * @return string|false Hook name of the current action, false if no action is running. + * + * @phpstan-return non-empty-string|false */ function current_action() { return current_filter(); @@ -688,6 +704,8 @@ function doing_action( $hook_name = null ) { * * @param string $hook_name The name of the action hook. * @return int The number of times the action hook has been fired. + * + * @phpstan-return int<0, max> */ function did_action( $hook_name ) { global $wp_actions; @@ -725,6 +743,8 @@ function did_action( $hook_name ) { * @param string $replacement Optional. The hook that should have been used. Default empty. * @param string $message Optional. A message regarding the change. Default empty. * @return mixed The filtered value after all hooked functions are applied to it. + * + * @phpstan-param non-empty-string $hook_name */ function apply_filters_deprecated( $hook_name, $args, $version, $replacement = '', $message = '' ) { if ( ! has_filter( $hook_name ) ) { @@ -752,6 +772,8 @@ function apply_filters_deprecated( $hook_name, $args, $version, $replacement = ' * @param string $version The version of WordPress that deprecated the hook. * @param string $replacement Optional. The hook that should have been used. Default empty. * @param string $message Optional. A message regarding the change. Default empty. + * + * @phpstan-param non-empty-string $hook_name */ function do_action_deprecated( $hook_name, $args, $version, $replacement = '', $message = '' ) { if ( ! has_action( $hook_name ) ) { @@ -882,6 +904,9 @@ function plugin_dir_url( $file ) { * * @param string $file The filename of the plugin including the path. * @param callable $callback The function hooked to the 'activate_PLUGIN' action. + * + * @phpstan-param callable(bool): void $callback + * @phpstan-return void */ function register_activation_hook( $file, $callback ) { $file = plugin_basename( $file ); @@ -905,6 +930,9 @@ function register_activation_hook( $file, $callback ) { * * @param string $file The filename of the plugin including the path. * @param callable $callback The function hooked to the 'deactivate_PLUGIN' action. + * + * @phpstan-param callable(bool): void $callback + * @phpstan-return void */ function register_deactivation_hook( $file, $callback ) { $file = plugin_basename( $file ); @@ -936,6 +964,8 @@ function register_deactivation_hook( $file, $callback ) { * @param string $file Plugin file. * @param callable $callback The callback to run when the hook is called. Must be * a static method or function. + * + * @phpstan-param callable(): void $callback */ function register_uninstall_hook( $file, $callback ) { if ( is_array( $callback ) && is_object( $callback[0] ) ) { diff --git a/tests/phpstan/baselines/parameter.defaultValue.neon b/tests/phpstan/baselines/parameter.defaultValue.neon index 34b6b678fc447..a8d29e0ec38e6 100644 --- a/tests/phpstan/baselines/parameter.defaultValue.neon +++ b/tests/phpstan/baselines/parameter.defaultValue.neon @@ -33,71 +33,6 @@ parameters: identifier: parameter.defaultValue count: 1 path: ../../../src/wp-admin/includes/class-custom-image-header.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_comments_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_dashboard_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_links_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_management_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_media_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_menu_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_options_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_pages_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_plugins_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_posts_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_theme_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#5 \$callback \(''''\) of function add_users_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - - - message: '#^Default value of the parameter \#6 \$callback \(''''\) of function add_submenu_page\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - message: '#^Default value of the parameter \#3 \$deprecated \(''''\) of function unregister_setting\(\) is incompatible with type callable\(\)\: mixed\.$#' identifier: parameter.defaultValue From 92edbf76101c4a098303e27f308f937930957a88 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:48:17 +0000 Subject: [PATCH 05/24] HTTP API: Add the PHPStan types the stubs carry for the request functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. The response array `wp_remote_get()` and its siblings return, which `WP_Http::request()` documents in prose and every caller reaches into, becomes the array shape it is: headers, body, response code and message, cookies, filename and the `WP_HTTP_Requests_Response` behind it. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-includes/class-wp-http.php | 27 +++++++++++ src/wp-includes/http.php | 76 +++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/wp-includes/class-wp-http.php b/src/wp-includes/class-wp-http.php index 13b82d95bfbd2..20d61ebf98cfb 100644 --- a/src/wp-includes/class-wp-http.php +++ b/src/wp-includes/class-wp-http.php @@ -633,6 +633,15 @@ private function _dispatch_request( $url, $args ) { * @param string|array $args Optional. Override the defaults. * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. * A WP_Error instance upon error. See WP_Http::response() for details. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ public function post( $url, $args = array() ) { $defaults = array( 'method' => 'POST' ); @@ -651,6 +660,15 @@ public function post( $url, $args = array() ) { * @param string|array $args Optional. Override the defaults. * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. * A WP_Error instance upon error. See WP_Http::response() for details. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ public function get( $url, $args = array() ) { $defaults = array( 'method' => 'GET' ); @@ -669,6 +687,15 @@ public function get( $url, $args = array() ) { * @param string|array $args Optional. Override the defaults. * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. * A WP_Error instance upon error. See WP_Http::response() for details. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ public function head( $url, $args = array() ) { $defaults = array( 'method' => 'HEAD' ); diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index c2855a8d8d9c1..98b6cf174a367 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -48,6 +48,15 @@ function _wp_http_get_object() { * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ function wp_safe_remote_request( $url, $args = array() ) { $args['reject_unsafe_urls'] = true; @@ -77,6 +86,15 @@ function wp_safe_remote_request( $url, $args = array() ) { * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ function wp_safe_remote_get( $url, $args = array() ) { $args['reject_unsafe_urls'] = true; @@ -106,6 +124,15 @@ function wp_safe_remote_get( $url, $args = array() ) { * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ function wp_safe_remote_post( $url, $args = array() ) { $args['reject_unsafe_urls'] = true; @@ -135,6 +162,15 @@ function wp_safe_remote_post( $url, $args = array() ) { * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ function wp_safe_remote_head( $url, $args = array() ) { $args['reject_unsafe_urls'] = true; @@ -162,6 +198,15 @@ function wp_safe_remote_head( $url, $args = array() ) { * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response array or a WP_Error on failure. * See WP_Http::request() for information on return value. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ function wp_remote_request( $url, $args = array() ) { $http = _wp_http_get_object(); @@ -183,6 +228,15 @@ function wp_remote_request( $url, $args = array() ) { * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ function wp_remote_get( $url, $args = array() ) { $http = _wp_http_get_object(); @@ -204,6 +258,15 @@ function wp_remote_get( $url, $args = array() ) { * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ function wp_remote_post( $url, $args = array() ) { $http = _wp_http_get_object(); @@ -225,6 +288,15 @@ function wp_remote_post( $url, $args = array() ) { * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. + * + * @phpstan-return array{ + * headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, + * body: string, + * response: array{code: int, message: string}, + * cookies: array, + * filename: string|null, + * http_response: WP_HTTP_Requests_Response, + * }|WP_Error */ function wp_remote_head( $url, $args = array() ) { $http = _wp_http_get_object(); @@ -555,6 +627,10 @@ function send_origin_headers() { * * @param string $url Request URL. * @return string|false Returns false if the URL is not safe, or the original URL if it is safe. + * + * @phpstan-template TUrl of string + * @phpstan-param TUrl $url + * @phpstan-return (TUrl is numeric|'' ? false : TUrl|false) */ function wp_http_validate_url( $url ) { if ( ! is_string( $url ) || '' === $url || is_numeric( $url ) ) { From 3306915fc1363ceb9cc5cffef54318c9b75fc487 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:49:11 +0000 Subject: [PATCH 06/24] Taxonomy: Add the PHPStan types the stubs carry for the taxonomy and category functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `get_term()`, `get_term_by()`, `get_category()` and `get_bookmark()`-style `$output` parameters resolve to the object, the associative array or the list they are asked for, and `sanitize_term()` and `sanitize_term_field()` keep the type they were given. The map states `get_tags()` as returning a `numeric-string` for `fields => count`, which core's own `get_terms()` type does not allow, so that branch is left out. `get_term_to_edit()` documents the `WP_Term` that `sanitize_term()` now says it returns, rather than the `int|string|null` it never returns. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-admin/includes/taxonomy.php | 10 ++++ src/wp-includes/category-template.php | 6 +++ src/wp-includes/category.php | 46 +++++++++++++++++++ src/wp-includes/taxonomy.php | 31 ++++++++++++- tests/phpstan/baselines/isset.property.neon | 2 +- .../phpstan/baselines/property.nonObject.neon | 25 ---------- .../phpstan/baselines/property.notFound.neon | 5 ++ .../phpstan/baselines/return.unusedType.neon | 10 ---- 8 files changed, 98 insertions(+), 37 deletions(-) diff --git a/src/wp-admin/includes/taxonomy.php b/src/wp-admin/includes/taxonomy.php index 470d36d55ffb1..85c7095183d7c 100644 --- a/src/wp-admin/includes/taxonomy.php +++ b/src/wp-admin/includes/taxonomy.php @@ -117,6 +117,8 @@ function wp_create_categories( $categories, $post_id = 0 ) { * @param bool $wp_error Optional. Default false. * @return int|WP_Error The ID number of the new or updated Category on success. Zero or a WP_Error on failure, * depending on param `$wp_error`. + * + * @phpstan-return ($wp_error is false ? int<0, max> : int<1, max>|WP_Error) */ function wp_insert_category( $catarr, $wp_error = false ) { $cat_defaults = array( @@ -184,6 +186,8 @@ function wp_insert_category( $catarr, $wp_error = false ) { * * @param array $catarr The 'cat_ID' value is required. All other keys are optional. * @return int|false The ID number of the new or updated Category on success. Zero or FALSE on failure. + * + * @phpstan-return int<0, max>|false */ function wp_update_category( $catarr ) { $cat_id = (int) $catarr['cat_ID']; @@ -218,6 +222,12 @@ function wp_update_category( $catarr ) { * @return mixed Returns null if the term does not exist. * Returns an array of the term ID and the term taxonomy ID if the pairing exists. * Returns 0 if term ID 0 is passed to the function. + * + * @phpstan-return ( + * $tag_name is 0 + * ? 0 + * : ($tag_name is '' ? null : array{term_id: string, term_taxonomy_id: string}|null) + * ) */ function tag_exists( $tag_name ) { return term_exists( $tag_name, 'post_tag' ); diff --git a/src/wp-includes/category-template.php b/src/wp-includes/category-template.php index f268f93cbc461..3f87eff239b2a 100644 --- a/src/wp-includes/category-template.php +++ b/src/wp-includes/category-template.php @@ -43,6 +43,8 @@ function get_category_link( $category ) { * @param bool $nicename Optional. Whether to use nice name for display. Default false. * @param array $deprecated Not used. * @return string|WP_Error A list of category parents on success, WP_Error on failure. + * + * @phpstan-param array{} $deprecated */ function get_category_parents( $category_id, $link = false, $separator = '/', $nicename = false, $deprecated = array() ) { @@ -534,6 +536,8 @@ function wp_dropdown_categories( $args = '' ) { * } * @return void|string|false Void if 'echo' argument is true, HTML list of categories if 'echo' is false. * False if the taxonomy does not exist. + * + * @phpstan-return ($args is array{echo: false|0, ...} ? string|false : false|void) */ function wp_list_categories( $args = '' ) { $defaults = array( @@ -849,6 +853,8 @@ function default_topic_count_scale( $count ) { * 0, 1, or their bool equivalents. * } * @return string|string[] Tag cloud as a string or an array, depending on 'format' argument. + * + * @phpstan-return ($args is array{format: 'array', ...} ? array : string) */ function wp_generate_tag_cloud( $tags, $args = '' ) { $defaults = array( diff --git a/src/wp-includes/category.php b/src/wp-includes/category.php index dbb48d630b076..eb9f83a2ed156 100644 --- a/src/wp-includes/category.php +++ b/src/wp-includes/category.php @@ -22,6 +22,24 @@ * @type string $taxonomy Taxonomy to retrieve terms for. Default 'category'. * } * @return array List of category objects. + * + * @phpstan-return ( + * $args is array{fields: 'count', ...} + * ? list + * : ( + * $args is array{fields: 'names'|'slugs', ...} + * ? list + * : ( + * $args is array{fields: 'id=>name'|'id=>slug', ...} + * ? array + * : ( + * $args is array{fields: 'id=>parent', ...} + * ? array + * : ($args is array{fields: 'ids'|'tt_ids', ...} ? list : array) + * ) + * ) + * ) + * ) */ function get_categories( $args = '' ) { $defaults = array( 'taxonomy' => 'category' ); @@ -88,6 +106,9 @@ function get_categories( $args = '' ) { * @return WP_Term|array|WP_Error|null Category data in type defined by $output parameter. * Returns a WP_Term object with backwards compatible property aliases filled in. * WP_Error if $category is empty, null if it does not exist. + * + * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output + * @phpstan-return ($category is object ? array|WP_Term : array|WP_Term|WP_Error|null) & ($output is 'ARRAY_A' ? array|WP_Error|null : ($output is 'ARRAY_N' ? array|WP_Error|null : WP_Term|WP_Error|null)) */ function get_category( $category, $output = OBJECT, $filter = 'raw' ) { $category = get_term( $category, 'category', $output, $filter ); @@ -121,6 +142,13 @@ function get_category( $category, $output = OBJECT, $filter = 'raw' ) { * correspond to a WP_Term object, an associative array, or a numeric array, * respectively. Default OBJECT. * @return WP_Term|array|WP_Error|null Type is based on $output value. + * + * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output + * @phpstan-return ( + * $output is 'ARRAY_A' + * ? array|WP_Error|null + * : ($output is 'ARRAY_N' ? array|WP_Error|null : WP_Term|WP_Error|null) + * ) */ function get_category_by_path( $category_path, $full_match = true, $output = OBJECT ) { $category_path = rawurlencode( urldecode( $category_path ) ); @@ -259,6 +287,10 @@ function cat_is_ancestor_of( $cat1, $cat2 ) { * @param object|array $category Category data. * @param string $context Optional. Default 'display'. * @return object|array Same type as $category with sanitized data for safe use. + * + * @phpstan-template T of array|object + * @phpstan-param T $category + * @phpstan-return T */ function sanitize_category( $category, $context = 'display' ) { return sanitize_term( $category, 'category', $context ); @@ -293,6 +325,20 @@ function sanitize_category_field( $field, $value, $cat_id, $context ) { * } * @return WP_Term[]|int|WP_Error Array of 'post_tag' term objects, a count thereof, * or WP_Error if any of the taxonomies do not exist. + * + * @phpstan-return ( + * $args is array{fields: 'names'|'slugs', ...} + * ? list + * : ( + * $args is array{fields: 'id=>name'|'id=>slug', ...} + * ? array + * : ( + * $args is array{fields: 'id=>parent', ...} + * ? array + * : ( $args is array{fields: 'ids'|'tt_ids', ...} ? list : array ) + * ) + * ) + * )|WP_Error */ function get_tags( $args = '' ) { $defaults = array( 'taxonomy' => 'post_tag' ); diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 56f12f7013dcb..c436c2864ebe7 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -374,6 +374,9 @@ function get_taxonomy( $taxonomy ) { * * @param string $taxonomy Name of taxonomy object. * @return bool Whether the taxonomy exists. + * + * @phpstan-assert-if-true =non-falsy-string $taxonomy + * @phpstan-return ($taxonomy is non-falsy-string ? bool : false) */ function taxonomy_exists( $taxonomy ) { global $wp_taxonomies; @@ -979,6 +982,13 @@ function get_tax_sql( $tax_query, $primary_table, $primary_id_column ) { * @param string $filter Optional. How to sanitize term fields. Default 'raw'. * @return WP_Term|array|WP_Error|null WP_Term instance (or array) on success, depending on the `$output` value. * WP_Error if `$taxonomy` does not exist. Null for miscellaneous failure. + * + * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output + * @phpstan-return ( + * $output is 'ARRAY_A' + * ? array|WP_Error|null + * : ($output is 'ARRAY_N' ? list|WP_Error|null : WP_Term|WP_Error|null) + * ) */ function get_term( $term, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) { if ( empty( $term ) ) { @@ -1101,6 +1111,12 @@ function get_term( $term, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) { * @param string $filter Optional. How to sanitize term fields. Default 'raw'. * @return WP_Term|array|false WP_Term instance (or array) on success, depending on the `$output` value. * False if `$taxonomy` does not exist or `$term` was not found. + * + * @phpstan-return false|( + * $output is 'ARRAY_A' + * ? array + * : ($output is 'ARRAY_N' ? list : WP_Term) + * ) */ function get_term_by( $field, $value, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) { @@ -1251,7 +1267,7 @@ 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 Sanitized term, an empty string if `$id` is not a term, or WP_Error on failure. */ function get_term_to_edit( $id, $taxonomy ) { $term = get_term( $id, $taxonomy ); @@ -1768,6 +1784,10 @@ function term_is_ancestor_of( $term1, $term2, $taxonomy ) { * Accepts 'raw', 'edit', 'db', 'display', 'rss', * 'attribute', or 'js'. Default 'display'. * @return array|object Term with all fields sanitized. + * + * @phpstan-template T of array|object + * @phpstan-param T $term + * @phpstan-return T */ function sanitize_term( $term, $taxonomy, $context = 'display' ) { $fields = array( 'term_id', 'name', 'description', 'slug', 'count', 'parent', 'term_group', 'term_taxonomy_id', 'object_id' ); @@ -1820,6 +1840,14 @@ function sanitize_term( $term, $taxonomy, $context = 'display' ) { * Accepts 'raw', 'edit', 'db', 'display', 'rss', * 'attribute', or 'js'. * @return mixed Sanitized field. + * + * @phpstan-template T of string + * @phpstan-param T $value + * @phpstan-return ( + * $field is 'parent'|'term_id'|'count'|'term_group'|'term_taxonomy_id'|'object_id' + * ? int<0, max> + * : ($context is 'raw' ? T : ($context is 'attribute'|'edit'|'js' ? string : mixed)) + * ) */ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { $int_fields = array( 'parent', 'term_id', 'count', 'term_group', 'term_taxonomy_id', 'object_id' ); @@ -1999,6 +2027,7 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { * the integer 0 when the queried parent term is not in the taxonomy * hierarchy, or WP_Error if the taxonomy does not exist. * @phpstan-return numeric-string|0|WP_Error + * @phpstan-param '' $deprecated */ function wp_count_terms( $args = array(), $deprecated = '' ) { $use_legacy_args = false; diff --git a/tests/phpstan/baselines/isset.property.neon b/tests/phpstan/baselines/isset.property.neon index ab7d839e22002..f5e272b598f34 100644 --- a/tests/phpstan/baselines/isset.property.neon +++ b/tests/phpstan/baselines/isset.property.neon @@ -186,7 +186,7 @@ parameters: - message: '#^Property WP_Term\:\:\$term_id \(int\) in isset\(\) is not nullable\.$#' identifier: isset.property - count: 1 + count: 2 path: ../../../src/wp-includes/nav-menu.php - message: '#^Property WP_Post\:\:\$post_name \(string\) in isset\(\) is not nullable\.$#' diff --git a/tests/phpstan/baselines/property.nonObject.neon b/tests/phpstan/baselines/property.nonObject.neon index d242b9373685b..e6617a1a2e336 100644 --- a/tests/phpstan/baselines/property.nonObject.neon +++ b/tests/phpstan/baselines/property.nonObject.neon @@ -73,16 +73,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 @@ -183,11 +173,6 @@ parameters: identifier: property.nonObject count: 1 path: ../../../src/wp-includes/class-wp-customize-manager.php - - - message: '#^Cannot access property \$object_id on array\|WP_Error\|WP_Term\.$#' - identifier: property.nonObject - count: 1 - path: ../../../src/wp-includes/class-wp-term-query.php - message: '#^Cannot access property \$term_id on string\|WP_Customize_Setting\.$#' identifier: property.nonObject @@ -218,18 +203,8 @@ parameters: identifier: property.nonObject count: 1 path: ../../../src/wp-includes/rest-api/endpoints/class-wp-rest-plugins-controller.php - - - message: '#^Cannot access property \$parent on array\|object\.$#' - identifier: property.nonObject - count: 1 - path: ../../../src/wp-includes/taxonomy.php - message: '#^Cannot access property \$template_name on array\.$#' identifier: property.nonObject count: 1 path: ../../../src/wp-includes/taxonomy.php - - - message: '#^Cannot access property \$term_id on array\|object\.$#' - identifier: property.nonObject - count: 4 - path: ../../../src/wp-includes/taxonomy.php diff --git a/tests/phpstan/baselines/property.notFound.neon b/tests/phpstan/baselines/property.notFound.neon index c7d60d13061d2..460a5082ab0b8 100644 --- a/tests/phpstan/baselines/property.notFound.neon +++ b/tests/phpstan/baselines/property.notFound.neon @@ -293,6 +293,11 @@ parameters: identifier: property.notFound count: 2 path: ../../../src/wp-includes/class-walker-nav-menu.php + - + message: '#^Access to an undefined property WP_Error\|WP_Term\:\:\$object_id\.$#' + identifier: property.notFound + count: 1 + path: ../../../src/wp-includes/class-wp-term-query.php - message: '#^Access to an undefined property WP_Query\:\:\$comments_by_type\.$#' identifier: property.notFound diff --git a/tests/phpstan/baselines/return.unusedType.neon b/tests/phpstan/baselines/return.unusedType.neon index 2acebf529a781..a1a27541a6f7c 100644 --- a/tests/phpstan/baselines/return.unusedType.neon +++ b/tests/phpstan/baselines/return.unusedType.neon @@ -78,16 +78,6 @@ parameters: 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 From 6e6ce0375f97cb9373807e77983d13f4b208851a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:49:51 +0000 Subject: [PATCH 07/24] Posts, Post Types: Add the PHPStan types the stubs carry for the post functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `register_post_type()` takes a lowercase name, `wp_insert_post()` and `wp_update_post()` return a `WP_Error` only when asked to, `wp_is_post_revision()` never returns a negative ID, and `WP_Query` reads two properties and calls one method that are not declared on it. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-admin/includes/post.php | 2 ++ src/wp-includes/class-wp-query.php | 4 ++++ src/wp-includes/post-template.php | 4 ++++ src/wp-includes/post.php | 2 ++ src/wp-includes/revision.php | 8 ++++++++ 5 files changed, 20 insertions(+) diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index 40a51e63945b3..2ce642cc2b02f 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -1206,6 +1206,8 @@ function _fix_attachment_links( $post ) { * * @param string $type The post_type you want the statuses for. Default 'post'. * @return string[] An array of all the statuses for the supplied post type. + * + * @phpstan-return list */ function get_available_post_statuses( $type = 'post' ) { $statuses = wp_count_posts( $type ); diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 2835da604bef5..632d41dcbdd79 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -14,6 +14,10 @@ * * @since 1.5.0 * @since 4.5.0 Removed the `$comments_popup` property. + * + * @phpstan-property-read bool $query_vars_changed + * @phpstan-property-read bool|string $query_vars_hash + * @phpstan-method void init_query_flags() */ #[AllowDynamicProperties] class WP_Query { diff --git a/src/wp-includes/post-template.php b/src/wp-includes/post-template.php index b817653aaf0ec..20a5424e362a1 100644 --- a/src/wp-includes/post-template.php +++ b/src/wp-includes/post-template.php @@ -1640,6 +1640,8 @@ function walk_page_dropdown_tree( ...$args ) { * @param bool $fullsize Optional. Whether to use full size. Default false. * @param bool $deprecated Deprecated. Not used. * @param bool $permalink Optional. Whether to include permalink. Default false. + * + * @phpstan-param false $deprecated */ function the_attachment_link( $post = 0, $fullsize = false, $deprecated = false, $permalink = false ) { if ( ! empty( $deprecated ) ) { @@ -2054,6 +2056,8 @@ function wp_post_revision_title_expanded( $revision, $link = true ) { * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @param string $type 'all' (default), 'revision' or 'autosave' + * + * @phpstan-param 'all'|'revision'|'autosave' $type */ function wp_list_post_revisions( $post = 0, $type = 'all' ) { $post = get_post( $post ); diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 89ab25ab93f80..b3842070b76fd 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -1828,6 +1828,8 @@ function get_post_types( $args = array(), $output = 'names', $operator = 'and' ) * } * @return WP_Post_Type|WP_Error The registered post type object on success, * WP_Error object on failure. + * + * @phpstan-param lowercase-string&non-empty-string $post_type */ function register_post_type( $post_type, $args = array() ) { global $wp_post_types; diff --git a/src/wp-includes/revision.php b/src/wp-includes/revision.php index 6e27fad4fa0a4..499fcc38080d4 100644 --- a/src/wp-includes/revision.php +++ b/src/wp-includes/revision.php @@ -18,6 +18,8 @@ * for insertion as a post revision. Default empty array. * @param bool $deprecated Not used. * @return string[] Array of fields that can be versioned. + * + * @phpstan-param false $deprecated */ function _wp_post_revision_fields( $post = array(), $deprecated = false ) { static $fields = null; @@ -307,6 +309,12 @@ function wp_get_post_autosave( $post_id, $user_id = 0 ) { * * @param int|WP_Post $post Post ID or post object. * @return int|false ID of revision's parent on success, false if not a revision. + * + * @phpstan-return ( + * $post is WP_Post + * ? false|int<0, max> + * : ($post is int ? false : false|int<0, max>) + * ) */ function wp_is_post_revision( $post ) { $post = wp_get_post_revision( $post ); From f73bb673c156156a1287e5654f1638b2b613a0fc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:50:24 +0000 Subject: [PATCH 08/24] Comments: Add the PHPStan types the stubs carry for the comment functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `wp_get_comment_status()` returns one of four statuses or false, and `wp_update_comment()` returns a `WP_Error` only when asked to. The map states its success value as `0|1`, where the method behind it, `wpdb::update()`, returns the number of rows it changed, so that is what the type says. `trackback_url()` is left as it is: its `$deprecated_echo` parameter is already documented and already carries the `@phpstan-ignore` that the conditional return needs. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-includes/comment-template.php | 5 +++++ src/wp-includes/comment.php | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index f7068362174e2..cc2608d4220e1 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -893,6 +893,9 @@ function get_comments_link( $post = 0 ) { * * @param string $deprecated Not Used. * @param string $deprecated_2 Not Used. + * + * @phpstan-param '' $deprecated + * @phpstan-param '' $deprecated_2 */ function comments_link( $deprecated = '', $deprecated_2 = '' ) { if ( ! empty( $deprecated ) ) { @@ -1279,6 +1282,8 @@ function trackback_url( $deprecated_echo = true ) { * @since 0.71 * * @param int|string $deprecated Not used (Was $timezone = 0). + * + * @phpstan-param '' $deprecated */ function trackback_rdf( $deprecated = '' ) { if ( ! empty( $deprecated ) ) { diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 8293c8b750314..68f95dd3cdf66 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -1896,6 +1896,8 @@ function wp_unspam_comment( $comment_id ) { * * @param int|WP_Comment $comment_id Comment ID or WP_Comment object * @return string|false Status might be 'trash', 'approved', 'unapproved', 'spam'. False on failure. + * + * @phpstan-return 'approved'|'spam'|'trash'|'unapproved'|false */ function wp_get_comment_status( $comment_id ) { $comment = get_comment( $comment_id ); @@ -2803,6 +2805,8 @@ function wp_send_note_notification( WP_User $user, WP_Comment $comment, ?WP_Post * @param string $comment_status New comment status, either 'hold', 'approve', 'spam', or 'trash'. * @param bool $wp_error Whether to return a WP_Error object if there is a failure. Default false. * @return bool|WP_Error True on success, false or WP_Error on failure. + * + * @phpstan-return ($wp_error is false ? bool : true|WP_Error) */ function wp_set_comment_status( $comment_id, $comment_status, $wp_error = false ) { global $wpdb; @@ -2877,6 +2881,8 @@ function wp_set_comment_status( $comment_id, $comment_status, $wp_error = false * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @return int|false|WP_Error The value 1 if the comment was updated, 0 if not updated. * False or a WP_Error object on failure. + * + * @phpstan-return ( $wp_error is false ? int|false : int|WP_Error ) */ function wp_update_comment( $commentarr, $wp_error = false ) { global $wpdb; @@ -3192,6 +3198,8 @@ function wp_update_comment_count_now( $post_id ) { * @param string $url URL to ping. * @param string $deprecated Not Used. * @return string|false String containing URI on success, false on failure. + * + * @phpstan-param '' $deprecated */ function discover_pingback_server_uri( $url, $deprecated = '' ) { if ( ! empty( $deprecated ) ) { From 393a85ff22ea418c42518e705f9bfec96d8cd9f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:50:51 +0000 Subject: [PATCH 09/24] Users: Add the PHPStan types the stubs carry for the user and pluggable functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `get_user_by()` cannot find a user by a non-positive ID, `wp_create_nonce()` and `wp_hash()` return a lowercase hash, the nonce functions take `-1` as well as a string action, and `wp_mail()`, `wp_rand()` and `wp_generate_password()` are marked impure so PHPStan does not treat repeated calls as equal. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-includes/author-template.php | 7 +++++++ src/wp-includes/pluggable.php | 20 ++++++++++++++++++++ src/wp-includes/user.php | 8 ++++++++ 3 files changed, 35 insertions(+) diff --git a/src/wp-includes/author-template.php b/src/wp-includes/author-template.php index b27bbf62379d4..014adad971ca5 100644 --- a/src/wp-includes/author-template.php +++ b/src/wp-includes/author-template.php @@ -20,6 +20,8 @@ * * @param string $deprecated Deprecated. * @return string The author's display name, empty string if unknown. + * + * @phpstan-param '' $deprecated */ function get_the_author( $deprecated = '' ) { global $authordata; @@ -57,6 +59,9 @@ function get_the_author( $deprecated = '' ) { * @param string $deprecated Deprecated. * @param bool $deprecated_echo Deprecated. Use get_the_author(). Echo the string or return it. * @return string The author's display name, from get_the_author(). + * + * @phpstan-param '' $deprecated + * @phpstan-param true $deprecated_echo */ function the_author( $deprecated = '', $deprecated_echo = true ) { if ( ! empty( $deprecated ) ) { @@ -362,6 +367,8 @@ function get_the_author_posts_link() { * @since 4.4.0 Converted into a wrapper for get_the_author_posts_link() * * @param string $deprecated Unused. + * + * @phpstan-param '' $deprecated */ function the_author_posts_link( $deprecated = '' ) { if ( ! empty( $deprecated ) ) { diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index aa39c31d78ce5..ac99a9742117b 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -97,6 +97,8 @@ function get_userdata( $user_id ) { * @param string $field The field to retrieve the user with. id | ID | slug | email | login. * @param int|string $value A value for $field. A user ID, slug, email address, or login name. * @return WP_User|false WP_User object on success, false on failure. + * + * @phpstan-return ($field is 'id'|'ID' ? ($value is int ? false : WP_User|false) : WP_User|false) */ function get_user_by( $field, $value ) { $userdata = WP_User::get_data_by( $field, $value ); @@ -185,6 +187,8 @@ function cache_users( $user_ids ) { * @param string|string[] $attachments Optional. Paths to files to attach. * @param string|string[] $embeds Optional. Paths to files to embed. * @return bool Whether the email was sent successfully. + * + * @phpstan-impure */ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array(), $embeds = array() ) { // Compact the input, apply the filters, and extract them back out. @@ -2440,6 +2444,9 @@ function wp_new_user_notification( $user_id, $deprecated = null, $notify = '' ) * * @param string|int $action Optional. The nonce action. Default -1. * @return float Float value rounded up to the next highest integer. + * + * @phpstan-param -1|string $action + * @phpstan-impure */ function wp_nonce_tick( $action = -1 ) { /** @@ -2470,6 +2477,9 @@ function wp_nonce_tick( $action = -1 ) { * @return int|false 1 if the nonce is valid and generated between 0-12 hours ago, * 2 if the nonce is valid and generated between 12-24 hours ago. * False if the nonce is invalid. + * + * @phpstan-param -1|string $action + * @phpstan-return 1|2|false */ function wp_verify_nonce( $nonce, $action = -1 ) { $nonce = (string) $nonce; @@ -2533,6 +2543,9 @@ function wp_verify_nonce( $nonce, $action = -1 ) { * * @param string|int $action Scalar value to add context to the nonce. * @return string The token. + * + * @phpstan-param -1|string $action + * @phpstan-return lowercase-string&non-falsy-string */ function wp_create_nonce( $action = -1 ) { $user = wp_get_current_user(); @@ -2715,6 +2728,9 @@ function wp_salt( $scheme = 'auth' ) { * @param string $scheme Authentication scheme (auth, secure_auth, logged_in, nonce). * @param string $algo Hashing algorithm to use. Default: 'md5'. * @return string Hash of $data. + * + * @phpstan-param 'auth'|'logged_in'|'nonce'|'secure_auth' $scheme + * @phpstan-return lowercase-string&non-falsy-string */ function wp_hash( $data, $scheme = 'auth', $algo = 'md5' ) { $salt = wp_salt( $scheme ); @@ -2960,6 +2976,8 @@ function wp_password_needs_rehash( $hash, $user_id = '' ) { * @param bool $extra_special_chars Optional. Whether to include other special characters. * Used when generating secret keys and salts. Default false. * @return string The random password. + * + * @phpstan-impure */ function wp_generate_password( $length = 12, $special_chars = true, $extra_special_chars = false ) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; @@ -3005,6 +3023,8 @@ function wp_generate_password( $length = 12, $special_chars = true, $extra_speci * @param int $max Optional. Upper limit for the generated number. * Accepts positive integers. Defaults to 4294967295. * @return int A random non-negative number between min and max. + * + * @phpstan-impure */ function wp_rand( $min = null, $max = null ) { global $rnd_value; diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index a13b3f75c0bdb..cedf080b1bd0e 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -722,6 +722,8 @@ function count_many_users_posts( $users, $post_type = 'post', $public_only = fal * @since MU (3.0.0) * * @return int The current user's ID, or 0 if no user is logged in. + * + * @phpstan-return int<0, max> */ function get_current_user_id() { if ( ! function_exists( 'wp_get_current_user' ) ) { @@ -749,6 +751,8 @@ function get_current_user_id() { * @param int $user Optional. User ID. * @param string $deprecated Use get_option() to check for an option in the options table. * @return mixed User option value on success, false on failure. + * + * @phpstan-param '' $deprecated */ function get_user_option( $option, $user = 0, $deprecated = '' ) { global $wpdb; @@ -854,6 +858,8 @@ function delete_user_option( $user_id, $option_name, $is_global = false ) { * * @param int $user_id User ID. * @return WP_User|false WP_User object on success, false on failure. + * + * @phpstan-return ($user_id is int ? false : WP_User|false) */ function get_user( $user_id ) { return get_user_by( 'id', $user_id ); @@ -3113,6 +3119,8 @@ function wp_get_password_hint() { * * @param WP_User $user User to retrieve password reset key for. * @return string|WP_Error Password reset key on success. WP_Error on error. + * + * @phpstan-impure */ function get_password_reset_key( $user ) { if ( ! ( $user instanceof WP_User ) ) { From cd83dd043632c59a51864764b17cd90d36f359a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:51:53 +0000 Subject: [PATCH 10/24] Administration: Add the PHPStan types the stubs carry for the admin functions and list tables. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `_get_list-table()` returns an instance of the list table it is asked for, which is what the template the map writes for it expresses, and `WP_List_Table::set_pagination_args()` takes the three keys it reads. The map has `_get_list_table()` accepting only a screen name in `$args`, where core passes a `WP_Screen` in three places and the function hands it to `convert_to_screen()`, which accepts one; the type says so. `iframe_header()` documents its second parameter as deprecated, and the one call in core still passing it is corrected, as is the `add_option()` call whose deprecated argument is null rather than the empty string it is documented as. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- phpstan.neon.dist | 1 - .../includes/class-wp-comments-list-table.php | 2 + src/wp-admin/includes/class-wp-list-table.php | 3 + src/wp-admin/includes/list-table.php | 9 +++ src/wp-admin/includes/menu.php | 5 ++ src/wp-admin/includes/meta-boxes.php | 2 + src/wp-admin/includes/template.php | 8 +++ src/wp-admin/includes/upgrade.php | 4 +- src/wp-admin/update.php | 2 +- src/wp-includes/class-wp-ajax-response.php | 2 + tests/phpstan/baselines/argument.type.neon | 5 -- tests/phpstan/baselines/arguments.count.neon | 20 ------- .../baselines/deadCode.unreachable.neon | 2 +- tests/phpstan/baselines/method.nonObject.neon | 55 ------------------- .../phpstan/baselines/property.protected.neon | 6 +- 15 files changed, 39 insertions(+), 87 deletions(-) delete mode 100644 tests/phpstan/baselines/method.nonObject.neon diff --git a/phpstan.neon.dist b/phpstan.neon.dist index cc4579365122b..98baf54d1d07d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -54,7 +54,6 @@ includes: - tests/phpstan/baselines/isset.offset.neon - tests/phpstan/baselines/isset.property.neon - tests/phpstan/baselines/method.childParameterType.neon - - tests/phpstan/baselines/method.nonObject.neon - tests/phpstan/baselines/method.unused.neon - tests/phpstan/baselines/notIdentical.alwaysTrue.neon - tests/phpstan/baselines/nullCoalesce.offset.neon diff --git a/src/wp-admin/includes/class-wp-comments-list-table.php b/src/wp-admin/includes/class-wp-comments-list-table.php index 2b927a7f81a6a..7294f89d1bd3b 100644 --- a/src/wp-admin/includes/class-wp-comments-list-table.php +++ b/src/wp-admin/includes/class-wp-comments-list-table.php @@ -62,6 +62,8 @@ public function __construct( $args = array() ) { * @param string $name Comment author name. * @param int $comment_id Comment ID. * @return string Avatar with the user name. + * + * @phpstan-return non-falsy-string */ public function floated_admin_avatar( $name, $comment_id ) { $comment = get_comment( $comment_id ); diff --git a/src/wp-admin/includes/class-wp-list-table.php b/src/wp-admin/includes/class-wp-list-table.php index df8e71834e0db..84f69c1f4c511 100644 --- a/src/wp-admin/includes/class-wp-list-table.php +++ b/src/wp-admin/includes/class-wp-list-table.php @@ -319,6 +319,9 @@ public function prepare_items() { * @since 3.1.0 * * @param array|string $args Array or string of arguments with information about the pagination. + * + * @phpstan-param array{total_items?: int, total_pages?: int, per_page?: int} $args + * @phpstan-return void */ protected function set_pagination_args( $args ) { $args = wp_parse_args( diff --git a/src/wp-admin/includes/list-table.php b/src/wp-admin/includes/list-table.php index 97dfe4f858c7e..ed1fd61a40050 100644 --- a/src/wp-admin/includes/list-table.php +++ b/src/wp-admin/includes/list-table.php @@ -17,6 +17,15 @@ * @param string $class_name The type of the list table, which is the class name. * @param array $args Optional. Arguments to pass to the class. Accepts 'screen'. * @return WP_List_Table|false List table object on success, false if the class does not exist. + * + * @phpstan-template T of string + * @phpstan-param T $class_name + * @phpstan-param array{screen?: string|WP_Screen|null} $args + * @phpstan-return ( + * $class_name is 'WP_Posts_List_Table'|'WP_Media_List_Table'|'WP_Terms_List_Table'|'WP_Users_List_Table'|'WP_Comments_List_Table'|'WP_Post_Comments_List_Table'|'WP_Links_List_Table'|'WP_Plugin_Install_List_Table'|'WP_Themes_List_Table'|'WP_Theme_Install_List_Table'|'WP_Plugins_List_Table'|'WP_Application_Passwords_List_Table'|'WP_MS_Sites_List_Table'|'WP_MS_Users_List_Table'|'WP_MS_Themes_List_Table'|'WP_Privacy_Data_Export_Requests_List_Table'|'WP_Privacy_Data_Removal_Requests_List_Table' + * ? new + * : false + * ) */ function _get_list_table( $class_name, $args = array() ) { $core_classes = array( diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index a95cf9e33956e..01bf5a046308b 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -208,6 +208,11 @@ * @param string $class_to_add The CSS class to add. * @param string $classes The string to add the CSS class to. * @return string The string with the CSS class added. + * + * @phpstan-template T of string + * @phpstan-param T $class_to_add + * @phpstan-pure + * @phpstan-return ($classes is empty ? T : non-empty-string) */ function add_cssclass( $class_to_add, $classes ) { if ( empty( $classes ) ) { diff --git a/src/wp-admin/includes/meta-boxes.php b/src/wp-admin/includes/meta-boxes.php index 535a00cd3fe94..9149f50daadd8 100644 --- a/src/wp-admin/includes/meta-boxes.php +++ b/src/wp-admin/includes/meta-boxes.php @@ -1257,6 +1257,8 @@ function link_target_meta_box( $link ) { * if it matches the current link's relationship. * Default empty string. * @param mixed $deprecated Deprecated. Not used. + * + * @phpstan-param '' $deprecated */ function xfn_check( $xfn_relationship, $xfn_value = '', $deprecated = '' ) { global $link; diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index a24aae32cc8dd..b29b7d481b489 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -1302,6 +1302,8 @@ function _get_plugin_from_callback( $callback ) { * Often this is the object that's the focus of the current screen, * for example a `WP_Post` or `WP_Comment` object. * @return int Number of meta_boxes. + * + * @phpstan-return int<0, max> */ function do_meta_boxes( $screen, $context, $data_object ) { global $wp_meta_boxes; @@ -1708,6 +1710,8 @@ function add_settings_section( $id, $title, $callback, $page, $args = array() ) * @type string $class CSS Class to be added to the `` element when the * field is output. * } + * + * @phpstan-return void */ function add_settings_field( $id, $title, $callback, $page, $section = 'default', $args = array() ) { global $wp_settings_fields; @@ -2124,6 +2128,8 @@ function _admin_search_query() { * * @param string $title Optional. Title of the Iframe page. Default empty. * @param bool $deprecated Not used. + * + * @phpstan-param false $deprecated */ function iframe_header( $title = '', $deprecated = false ) { global $hook_suffix, $admin_body_class, $body_id, $wp_locale; @@ -2633,6 +2639,8 @@ function submit_button( $text = '', $type = 'primary', $name = 'submit', $wrap = * e.g. `id="search-submit"`, though the array format is generally preferred. * Default empty string. * @return string Submit button HTML. + * + * @phpstan-return non-falsy-string */ function get_submit_button( $text = '', $type = 'primary large', $name = 'submit', $wrap = true, $other_attributes = '' ) { if ( ! is_array( $type ) ) { diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index 244401cf24013..461ab78a219e5 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -43,6 +43,8 @@ * @type string $password The password of the site owner, if their user account didn't already exist. * @type string $password_message The explanatory message regarding the password. * } + * + * @phpstan-param '' $deprecated */ function wp_install( $blog_title, @@ -1865,7 +1867,7 @@ function upgrade_340() { if ( 'yes' === $wpdb->get_var( "SELECT autoload FROM $wpdb->options WHERE option_name = 'uninstall_plugins'" ) ) { $uninstall_plugins = get_option( 'uninstall_plugins' ); delete_option( 'uninstall_plugins' ); - add_option( 'uninstall_plugins', $uninstall_plugins, null, false ); + add_option( 'uninstall_plugins', $uninstall_plugins, '', false ); } } } diff --git a/src/wp-admin/update.php b/src/wp-admin/update.php index a6c59ec06dab2..a0ba9cfc069b2 100644 --- a/src/wp-admin/update.php +++ b/src/wp-admin/update.php @@ -87,7 +87,7 @@ wp_redirect( admin_url( 'update.php?action=activate-plugin&success=true&plugin=' . urlencode( $plugin ) . '&_wpnonce=' . $_GET['_wpnonce'] ) ); die(); } - iframe_header( __( 'Plugin Reactivation' ), true ); + iframe_header( __( 'Plugin Reactivation' ) ); if ( isset( $_GET['success'] ) ) { echo '

' . __( 'Plugin reactivated successfully.' ) . '

'; } diff --git a/src/wp-includes/class-wp-ajax-response.php b/src/wp-includes/class-wp-ajax-response.php index ab747618e0fbf..b9946343ac79f 100644 --- a/src/wp-includes/class-wp-ajax-response.php +++ b/src/wp-includes/class-wp-ajax-response.php @@ -63,6 +63,8 @@ public function __construct( $args = '' ) { * element as CDATA. Default empty array. * } * @return string XML response. + * + * @phpstan-return non-falsy-string */ public function add( $args = '' ) { $defaults = array( diff --git a/tests/phpstan/baselines/argument.type.neon b/tests/phpstan/baselines/argument.type.neon index 4c0a9652d0e98..730bd359d4fd3 100644 --- a/tests/phpstan/baselines/argument.type.neon +++ b/tests/phpstan/baselines/argument.type.neon @@ -318,11 +318,6 @@ parameters: identifier: argument.type count: 1 path: ../../../src/wp-admin/includes/upgrade.php - - - message: '#^Parameter \#3 \$deprecated of function add_option expects string, null given\.$#' - identifier: argument.type - count: 1 - path: ../../../src/wp-admin/includes/upgrade.php - message: '#^Parameter \#1 \$bookmark_id of function clean_bookmark_cache expects int, string\|null given\.$#' identifier: argument.type diff --git a/tests/phpstan/baselines/arguments.count.neon b/tests/phpstan/baselines/arguments.count.neon index e3cab51b3621e..8a754d7469c7e 100644 --- a/tests/phpstan/baselines/arguments.count.neon +++ b/tests/phpstan/baselines/arguments.count.neon @@ -18,21 +18,6 @@ parameters: ignoreErrors: - - - message: '#^Method WP_List_Table\:\:display_rows\(\) invoked with 2 parameters, 0 required\.$#' - identifier: arguments.count - count: 1 - path: ../../../src/wp-admin/includes/ajax-actions.php - - - message: '#^Method WP_List_Table\:\:single_row\(\) invoked with 2 parameters, 1 required\.$#' - identifier: arguments.count - count: 2 - path: ../../../src/wp-admin/includes/ajax-actions.php - - - message: '#^Method WP_List_Table\:\:single_row\(\) invoked with 3 parameters, 1 required\.$#' - identifier: arguments.count - count: 1 - path: ../../../src/wp-admin/includes/ajax-actions.php - message: '#^Method WP_Upgrader_Skin\:\:before\(\) invoked with 1 parameter, 0 required\.$#' identifier: arguments.count @@ -43,8 +28,3 @@ parameters: identifier: arguments.count count: 2 path: ../../../src/wp-admin/includes/class-theme-upgrader.php - - - message: '#^Method WP_List_Table\:\:display\(\) invoked with 1 parameter, 0 required\.$#' - identifier: arguments.count - count: 1 - path: ../../../src/wp-admin/includes/meta-boxes.php diff --git a/tests/phpstan/baselines/deadCode.unreachable.neon b/tests/phpstan/baselines/deadCode.unreachable.neon index 8a4288bc40c43..9c8bebbc5db27 100644 --- a/tests/phpstan/baselines/deadCode.unreachable.neon +++ b/tests/phpstan/baselines/deadCode.unreachable.neon @@ -31,7 +31,7 @@ parameters: - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable - count: 3 + count: 2 path: ../../../src/wp-admin/includes/ajax-actions.php - message: '#^Unreachable statement \- code above always terminates\.$#' diff --git a/tests/phpstan/baselines/method.nonObject.neon b/tests/phpstan/baselines/method.nonObject.neon deleted file mode 100644 index 4d0727f8950f1..0000000000000 --- a/tests/phpstan/baselines/method.nonObject.neon +++ /dev/null @@ -1,55 +0,0 @@ -# PHPStan baseline for the `method.nonObject` errors in WordPress core. -# -# https://phpstan.org/error-identifiers/method.nonObject -# -# 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=method.nonObject -# -# which reruns the analysis with this file suppressed so the errors surface again. - -parameters: - ignoreErrors: - - - message: '#^Cannot call method inline_edit\(\) on WP_List_Table\|false\.$#' - identifier: method.nonObject - count: 1 - path: ../../../src/wp-admin/edit-tags.php - - - message: '#^Cannot call method inline_edit\(\) on WP_List_Table\|false\.$#' - identifier: method.nonObject - count: 1 - path: ../../../src/wp-admin/edit.php - - - message: '#^Cannot call method embed_scripts\(\) on WP_List_Table\|false\.$#' - identifier: method.nonObject - count: 1 - path: ../../../src/wp-admin/erase-personal-data.php - - - message: '#^Cannot call method process_bulk_action\(\) on WP_List_Table\|false\.$#' - identifier: method.nonObject - count: 1 - path: ../../../src/wp-admin/erase-personal-data.php - - - message: '#^Cannot call method embed_scripts\(\) on WP_List_Table\|false\.$#' - identifier: method.nonObject - count: 1 - path: ../../../src/wp-admin/export-personal-data.php - - - message: '#^Cannot call method process_bulk_action\(\) on WP_List_Table\|false\.$#' - identifier: method.nonObject - count: 1 - path: ../../../src/wp-admin/export-personal-data.php - - - message: '#^Cannot call method theme_installer_single\(\) on WP_List_Table\|false\.$#' - identifier: method.nonObject - count: 1 - path: ../../../src/wp-admin/includes/theme-install.php diff --git a/tests/phpstan/baselines/property.protected.neon b/tests/phpstan/baselines/property.protected.neon index b29d125bd2a7a..43fe90c187140 100644 --- a/tests/phpstan/baselines/property.protected.neon +++ b/tests/phpstan/baselines/property.protected.neon @@ -19,17 +19,17 @@ parameters: ignoreErrors: - - message: '#^Access to protected property WP_List_Table\:\:\$screen\.$#' + message: '#^Access to protected property WP_Privacy_Data_Removal_Requests_List_Table\:\:\$screen\.$#' identifier: property.protected count: 1 path: ../../../src/wp-admin/erase-personal-data.php - - message: '#^Access to protected property WP_List_Table\:\:\$screen\.$#' + message: '#^Access to protected property WP_Privacy_Data_Export_Requests_List_Table\:\:\$screen\.$#' identifier: property.protected count: 1 path: ../../../src/wp-admin/export-personal-data.php - - message: '#^Access to protected property WP_List_Table\:\:\$screen\.$#' + message: '#^Access to protected property WP_Posts_List_Table\:\:\$screen\.$#' identifier: property.protected count: 1 path: ../../../src/wp-admin/includes/ajax-actions.php From dc763b7dc0f5e295075152aa5783298587d19e64 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:53:02 +0000 Subject: [PATCH 11/24] Options, Meta APIs: Add the PHPStan types the stubs carry for the option, meta and cron functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. The cron functions take a list of arguments rather than an arbitrary array, and return a `WP_Error` only when asked to; `get_site_option()`, `get_user_option()` and `register_meta()` take a deprecated parameter that is documented but not typed. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-includes/cron.php | 23 +++++++++++++++++++ src/wp-includes/meta.php | 2 ++ src/wp-includes/option.php | 8 +++++++ .../baselines/parameter.defaultValue.neon | 5 ---- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/cron.php b/src/wp-includes/cron.php index 1070ae4680b91..2d73f92df04eb 100644 --- a/src/wp-includes/cron.php +++ b/src/wp-includes/cron.php @@ -44,6 +44,9 @@ * database performance issues. * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @return bool|WP_Error True if event successfully scheduled. False or WP_Error on failure. + * + * @phpstan-param list $args + * @phpstan-return ($wp_error is false ? bool : true|WP_Error) */ function wp_schedule_single_event( $timestamp, $hook, $args = array(), $wp_error = false ) { // Make sure timestamp is a positive integer. @@ -248,6 +251,9 @@ function wp_schedule_single_event( $timestamp, $hook, $args = array(), $wp_error * database performance issues. * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @return bool|WP_Error True if event successfully scheduled. False or WP_Error on failure. + * + * @phpstan-param list $args + * @phpstan-return ($wp_error is false ? bool : true|WP_Error) */ function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array(), $wp_error = false ) { // Make sure timestamp is a positive integer. @@ -363,6 +369,9 @@ function wp_schedule_event( $timestamp, $recurrence, $hook, $args = array(), $wp * database performance issues. * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @return bool|WP_Error True if event successfully rescheduled. False or WP_Error on failure. + * + * @phpstan-param list $args + * @phpstan-return ($wp_error is false ? bool : true|WP_Error) */ function wp_reschedule_event( $timestamp, $recurrence, $hook, $args = array(), $wp_error = false ) { // Make sure timestamp is a positive integer. @@ -485,6 +494,9 @@ function wp_reschedule_event( $timestamp, $recurrence, $hook, $args = array(), $ * arguments do not match exactly, the event will not be found. Default empty array. * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @return bool|WP_Error True if event successfully unscheduled. False or WP_Error on failure. + * + * @phpstan-param list $args + * @phpstan-return ($wp_error is false ? bool : true|WP_Error) */ function wp_unschedule_event( $timestamp, $hook, $args = array(), $wp_error = false ) { // Make sure timestamp is a positive integer. @@ -572,6 +584,9 @@ function wp_unschedule_event( $timestamp, $hook, $args = array(), $wp_error = fa * @return int|false|WP_Error On success an integer indicating number of events unscheduled (0 indicates no * events were registered with the hook and arguments combination), false or WP_Error * if unscheduling one or more events fail. + * + * @phpstan-param list $args + * @phpstan-return (int<0, max>|($wp_error is false ? false : WP_Error)) */ function wp_clear_scheduled_hook( $hook, $args = array(), $wp_error = false ) { /* @@ -677,6 +692,8 @@ function wp_clear_scheduled_hook( $hook, $args = array(), $wp_error = false ) { * @param bool $wp_error Optional. Whether to return a WP_Error on failure. Default false. * @return int|false|WP_Error On success an integer indicating number of events unscheduled (0 indicates no * events were registered on the hook), false or WP_Error if unscheduling fails. + * + * @phpstan-return ($wp_error is false ? int<0, max>|false : int<0, max>|WP_Error) */ function wp_unschedule_hook( $hook, $wp_error = false ) { /** @@ -774,6 +791,8 @@ function wp_unschedule_hook( $hook, $wp_error = false ) { * @type array $args Array containing each separate argument to pass to the hook's callback function. * @type int $interval Optional. The interval time in seconds for the schedule. Only present for recurring events. * } + * + * @phpstan-param list $args */ function wp_get_scheduled_event( $hook, $args = array(), $timestamp = null ) { /** @@ -855,6 +874,8 @@ function wp_get_scheduled_event( $hook, $args = array(), $timestamp = null ) { * event, so they must match those used when originally scheduling the event. If the * arguments do not match exactly, the event will not be found. Default empty array. * @return int|false The Unix timestamp (UTC) of the next time the event will occur. False if the event doesn't exist. + * + * @phpstan-param list $args */ function wp_next_scheduled( $hook, $args = array() ) { $next_event = wp_get_scheduled_event( $hook, $args ); @@ -1179,6 +1200,8 @@ function wp_get_schedules() { * @param array $args Optional. Arguments passed to the event's callback function. * Default empty array. * @return string|false Schedule name on success, false if no schedule. + * + * @phpstan-param list $args */ function wp_get_schedule( $hook, $args = array() ) { $schedule = false; diff --git a/src/wp-includes/meta.php b/src/wp-includes/meta.php index 62d28cdbd9f5c..27d343b7b5f0d 100644 --- a/src/wp-includes/meta.php +++ b/src/wp-includes/meta.php @@ -1466,6 +1466,8 @@ function sanitize_meta( $meta_key, $meta_value, $object_type, $object_subtype = * @return bool True if the meta key was successfully registered in the global array, false if not. * Registering a meta key with distinct sanitize and auth callbacks will fire those callbacks, * but will not add to the global registry. + * + * @phpstan-param null $deprecated */ function register_meta( $object_type, $meta_key, $args, $deprecated = null ) { global $wp_meta_keys; diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index d5c179c645af3..f32f518d35580 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -1065,6 +1065,8 @@ function update_option( $option, $value, $autoload = null ) { * to not autoload them, by using false. * Default is null, which means WordPress will determine the autoload value. * @return bool True if the option was added, false otherwise. + * + * @phpstan-param '' $deprecated */ function add_option( $option, $value = '', $deprecated = '', $autoload = null ) { global $wpdb; @@ -1427,6 +1429,8 @@ function delete_transient( $transient ) { * * @param string $transient Transient name. Expected to not be SQL-escaped. * @return mixed Value of transient. + * + * @phpstan-impure */ function get_transient( $transient ) { @@ -1930,6 +1934,8 @@ function delete_all_user_settings() { * @param mixed $default_value Optional. Value to return if the option doesn't exist. Default false. * @param bool $deprecated Whether to use cache. Multisite only. Always set to true. * @return mixed Value set for the option. + * + * @phpstan-param true $deprecated */ function get_site_option( $option, $default_value = false, $deprecated = true ) { return get_network_option( null, $option, $default_value ); @@ -3119,6 +3125,8 @@ function register_setting( $option_group, $option_name, $args = array() ) { * @param string $option_group The settings group name used during registration. * @param string $option_name The name of the option to unregister. * @param callable $deprecated Optional. Deprecated. + * + * @phpstan-param '' $deprecated */ function unregister_setting( $option_group, $option_name, $deprecated = '' ) { global $new_allowed_options, $wp_registered_settings; diff --git a/tests/phpstan/baselines/parameter.defaultValue.neon b/tests/phpstan/baselines/parameter.defaultValue.neon index a8d29e0ec38e6..15f49b79a9ff0 100644 --- a/tests/phpstan/baselines/parameter.defaultValue.neon +++ b/tests/phpstan/baselines/parameter.defaultValue.neon @@ -33,8 +33,3 @@ parameters: identifier: parameter.defaultValue count: 1 path: ../../../src/wp-admin/includes/class-custom-image-header.php - - - message: '#^Default value of the parameter \#3 \$deprecated \(''''\) of function unregister_setting\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-includes/option.php From 7b56c7b103187807911236fe5c3c87470ae144cd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:53:24 +0000 Subject: [PATCH 12/24] Networks and Sites: Add the PHPStan types the stubs carry for the multisite functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `get_sites()` returns a count, a list of IDs or a list of sites according to the arguments it is given, and `switch_to_blog()` takes a deprecated second parameter. `switch_to_blog()` takes the site ID as an int, and the one call in core passing `WP_Site::$blog_id`, which is documented as a string, now casts it. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-admin/includes/ms.php | 4 ++++ src/wp-includes/embed.php | 2 +- src/wp-includes/ms-blogs.php | 6 ++++++ src/wp-includes/ms-functions.php | 6 ++++++ src/wp-includes/ms-site.php | 6 ++++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/includes/ms.php b/src/wp-admin/includes/ms.php index 155170aaf4f41..211a3c2bbddff 100644 --- a/src/wp-admin/includes/ms.php +++ b/src/wp-admin/includes/ms.php @@ -1187,6 +1187,8 @@ function network_edit_site_nav( $args = array() ) { * @since 4.9.0 * * @return array Help tab arguments. + * + * @phpstan-return array{id: 'overview', title: string, content: non-falsy-string} */ function get_site_screen_help_tab_args() { return array( @@ -1211,6 +1213,8 @@ function get_site_screen_help_tab_args() { * @since 4.9.0 * * @return string Help sidebar content. + * + * @phpstan-return non-falsy-string */ function get_site_screen_help_sidebar_content() { return '

' . __( 'For more information:' ) . '

' . diff --git a/src/wp-includes/embed.php b/src/wp-includes/embed.php index e87cf4ec57989..6a7501027a978 100644 --- a/src/wp-includes/embed.php +++ b/src/wp-includes/embed.php @@ -676,7 +676,7 @@ function get_oembed_response_data_for_url( $url, $args ) { } if ( $site && get_current_blog_id() !== (int) $site->blog_id ) { - switch_to_blog( $site->blog_id ); + switch_to_blog( (int) $site->blog_id ); $switched_blog = true; } } diff --git a/src/wp-includes/ms-blogs.php b/src/wp-includes/ms-blogs.php index c54563fbbd2b8..2ac766ffc37ad 100644 --- a/src/wp-includes/ms-blogs.php +++ b/src/wp-includes/ms-blogs.php @@ -456,6 +456,8 @@ function delete_blog_option( $id, $option ) { * @param mixed $value The option value. * @param mixed $deprecated Not used. * @return bool True if the value was updated, false otherwise. + * + * @phpstan-param null $deprecated */ function update_blog_option( $id, $option, $value, $deprecated = null ) { $id = (int) $id; @@ -496,6 +498,8 @@ function update_blog_option( $id, $option, $value, $deprecated = null ) { * @param int $new_blog_id The ID of the blog to switch to. Default: current blog. * @param bool $deprecated Not used. * @return true Always returns true. + * + * @phpstan-param null $deprecated */ function switch_to_blog( $new_blog_id, $deprecated = null ) { global $wpdb; @@ -819,6 +823,8 @@ function get_blog_status( $id, $pref ) { * Can be used for pagination. Default 0. * @param int $quantity Optional. The maximum number of blogs to retrieve. Default 40. * @return array The list of blogs. + * + * @phpstan-param '' $deprecated */ function get_last_updated( $deprecated = '', $start = 0, $quantity = 40 ) { global $wpdb; diff --git a/src/wp-includes/ms-functions.php b/src/wp-includes/ms-functions.php index 81dda98f3e312..46fe2537d8521 100644 --- a/src/wp-includes/ms-functions.php +++ b/src/wp-includes/ms-functions.php @@ -1478,6 +1478,8 @@ function wpmu_create_blog( $domain, $path, $title, $user_id, $options = array(), * @param WP_Site|int $blog_id The new site's object or ID. * @param string $deprecated Not used. * @return bool + * + * @phpstan-param '' $deprecated */ function newblog_notify_siteadmin( $blog_id, $deprecated = '' ) { if ( is_object( $blog_id ) ) { @@ -2088,6 +2090,8 @@ function check_upload_mimes( $mimes ) { * @global wpdb $wpdb WordPress database abstraction object. * * @param string $deprecated Not used. + * + * @phpstan-param '' $deprecated */ function update_posts_count( $deprecated = '' ) { global $wpdb; @@ -2142,6 +2146,8 @@ function wpmu_log_new_registrations( $blog_id, $user_id ) { * * @type string $0 The current site's domain. * } + * + * @phpstan-param '' $deprecated */ function redirect_this_site( $deprecated = '' ) { return array( get_network()->domain ); diff --git a/src/wp-includes/ms-site.php b/src/wp-includes/ms-site.php index 8540b276218a9..abc72bc13ea86 100644 --- a/src/wp-includes/ms-site.php +++ b/src/wp-includes/ms-site.php @@ -441,6 +441,12 @@ function update_sitemeta_cache( $site_ids ) { * for information on accepted arguments. Default empty array. * @return WP_Site[]|int[]|int List of WP_Site objects, a list of site IDs when 'fields' is set to 'ids', * or the number of sites when 'count' is passed as a query var. + * + * @phpstan-return ( + * $args is array{count: true, ...} + * ? int + * : ($args is array{fields: 'ids', ...} ? array : array) + * ) */ function get_sites( $args = array() ) { $query = new WP_Site_Query(); From d64c6ac047a02c50b3cf38b201a938ced0296c2f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:54:02 +0000 Subject: [PATCH 13/24] Widgets: Add the PHPStan types the stubs carry for the widget functions and classes. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `WP_Widget` becomes generic in the shape of the instance it stores, so a widget that documents its own settings array has `widget()`, `update()` and `form()` typed in terms of it, and `register_widget()` takes the class name or instance it documents. The map has `wp_widget_rss_form()` receiving a bool `error` where the function and `wp_widget_rss_process()` both give it `string|false`, so the shape says that. `WP_Widget_Factory::register()` instantiates a widget class with no arguments, which `WP_Widget`'s own constructor does not accept; the call carries an `@phpstan-ignore` saying why, since every widget class declares a constructor of its own. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-admin/includes/widgets.php | 2 ++ src/wp-includes/class-wp-widget-factory.php | 6 ++++ src/wp-includes/class-wp-widget.php | 35 ++++++++++++++++++++ src/wp-includes/widgets.php | 36 +++++++++++++++++++++ tests/phpstan/baselines/argument.type.neon | 2 +- tests/phpstan/baselines/isset.property.neon | 2 +- 6 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/widgets.php b/src/wp-admin/includes/widgets.php index e751602866b0d..5697a6e9ff0dd 100644 --- a/src/wp-admin/includes/widgets.php +++ b/src/wp-admin/includes/widgets.php @@ -322,6 +322,8 @@ function wp_widget_control( $sidebar_args ) { /** * @param string $classes * @return string Modified body classes. + * + * @phpstan-return non-falsy-string */ function wp_widgets_access_body_class( $classes ) { return "$classes widgets_access "; diff --git a/src/wp-includes/class-wp-widget-factory.php b/src/wp-includes/class-wp-widget-factory.php index 1f6ed3a58919e..24d3e05de0550 100644 --- a/src/wp-includes/class-wp-widget-factory.php +++ b/src/wp-includes/class-wp-widget-factory.php @@ -35,6 +35,7 @@ class WP_Widget_Factory { * @since 2.8.0 * @var array * @phpstan-var array + * @phpstan-var array */ public $widgets = array(); @@ -69,11 +70,14 @@ public function WP_Widget_Factory() { * @since 7.1.1 The key for an instance is prefixed so that it is never cast to an integer. * * @param string|WP_Widget $widget Either the name of a `WP_Widget` subclass or an instance of a `WP_Widget` subclass. + * + * @phpstan-param class-string|WP_Widget $widget */ public function register( $widget ) { if ( $widget instanceof WP_Widget ) { $this->widgets[ self::INSTANCE_KEY_PREFIX . spl_object_id( $widget ) ] = $widget; } else { + // @phpstan-ignore arguments.count (Widget classes declare their own constructor, which takes no arguments.) $this->widgets[ $widget ] = new $widget(); } } @@ -87,6 +91,8 @@ public function register( $widget ) { * @since 7.1.1 The key for an instance is prefixed so that it is never cast to an integer. * * @param string|WP_Widget $widget Either the name of a `WP_Widget` subclass or an instance of a `WP_Widget` subclass. + * + * @phpstan-param class-string|WP_Widget $widget */ public function unregister( $widget ) { if ( $widget instanceof WP_Widget ) { diff --git a/src/wp-includes/class-wp-widget.php b/src/wp-includes/class-wp-widget.php index b131c50db3226..9ae4f908af073 100644 --- a/src/wp-includes/class-wp-widget.php +++ b/src/wp-includes/class-wp-widget.php @@ -16,6 +16,8 @@ * * @since 2.8.0 * @since 4.4.0 Moved to its own file from wp-includes/widgets.php + * + * @phpstan-template T of array = array */ #[AllowDynamicProperties] class WP_Widget { @@ -109,6 +111,23 @@ class WP_Widget { * @param array $args Display arguments including 'before_title', 'after_title', * 'before_widget', and 'after_widget'. * @param array $instance The settings for the particular instance of the widget. + * + * @phpstan-param T $instance + * @phpstan-param array{ + * name: string, + * id: string, + * description: string, + * class: string, + * before_widget: string, + * after_widget: string, + * before_title: string, + * after_title: string, + * before_sidebar: string, + * after_sidebar: string, + * show_in_rest: boolean, + * widget_id: string, + * widget_name: string, + * } $args */ public function widget( $args, $instance ) { die( 'function WP_Widget::widget() must be overridden in a subclass.' ); @@ -127,6 +146,9 @@ public function widget( $args, $instance ) { * WP_Widget::form(). * @param array $old_instance Old settings for this instance. * @return array Settings to save or bool false to cancel saving. + * + * @phpstan-param T $new_instance + * @phpstan-param T $old_instance */ public function update( $new_instance, $old_instance ) { return $new_instance; @@ -140,6 +162,8 @@ public function update( $new_instance, $old_instance ) { * @param array $instance The settings for the particular instance of the widget. * @return string|void Default return is 'noform'. A subclass which echoes its own * form returns nothing. + * + * @phpstan-param T $instance */ public function form( $instance ) { echo '

' . __( 'There are no options for this widget.' ) . '

'; @@ -213,6 +237,8 @@ public function WP_Widget( $id_base, $name, $widget_options = array(), $control_ * * @param string $field_name Field name. * @return string Name attribute for `$field_name`. + * + * @phpstan-return non-falsy-string */ public function get_field_name( $field_name ) { $pos = strpos( $field_name, '[' ); @@ -238,6 +264,8 @@ public function get_field_name( $field_name ) { * * @param string $field_name Field name. * @return string ID attribute for `$field_name`. + * + * @phpstan-return non-falsy-string */ public function get_field_id( $field_name ) { $field_name = str_replace( array( '[]', '[', ']' ), array( '', '-', '' ), $field_name ); @@ -357,6 +385,8 @@ public function is_preview() { * * @type int $number Number increment used for multiples of the same widget. * } + * + * @final */ public function display_callback( $args, $widget_args = 1 ) { if ( is_numeric( $widget_args ) ) { @@ -408,6 +438,9 @@ public function display_callback( $args, $widget_args = 1 ) { * @global array $wp_registered_widgets * * @param int $deprecated Not used. + * + * @phpstan-param 1 $deprecated + * @final */ public function update_callback( $deprecated = 1 ) { global $wp_registered_widgets; @@ -500,6 +533,8 @@ public function update_callback( $deprecated = 1 ) { * @type int $number Number increment used for multiples of the same widget. * } * @return string|null + * + * @final */ public function form_callback( $widget_args = 1 ) { if ( is_numeric( $widget_args ) ) { diff --git a/src/wp-includes/widgets.php b/src/wp-includes/widgets.php index 2acfdd8c07594..c0c5725aa7cc4 100644 --- a/src/wp-includes/widgets.php +++ b/src/wp-includes/widgets.php @@ -116,6 +116,9 @@ * @global WP_Widget_Factory $wp_widget_factory * * @param string|WP_Widget $widget Either the name of a `WP_Widget` subclass or an instance of a `WP_Widget` subclass. + * + * @phpstan-param class-string|WP_Widget $widget + * @phpstan-return void */ function register_widget( $widget ) { global $wp_widget_factory; @@ -1016,6 +1019,8 @@ function is_active_sidebar( $index ) { * * @param bool $deprecated Not used (argument deprecated). * @return array Upgraded list of widgets to version 3 array format when called from the admin. + * + * @phpstan-param true $deprecated */ function wp_get_sidebars_widgets( $deprecated = true ) { if ( true !== $deprecated ) { @@ -1707,6 +1712,26 @@ function wp_widget_rss_output( $rss, $args = array() ) { * * @param array|string $args Values for input fields. * @param array $inputs Override default display options. + * + * @phpstan-param array{ + * number: int, + * error: string|false, + * title?: string, + * url?: string, + * items?: int, + * show_summary?: int, + * show_author?: int, + * show_date?: int, + * } $args + * @phpstan-param array{ + * title?: bool, + * url?: bool, + * items?: bool, + * show_summary?: bool, + * show_author?: bool, + * show_date?: bool, + * } $inputs + * @phpstan-return void */ function wp_widget_rss_form( $args, $inputs = null ) { $default_inputs = array( @@ -1792,6 +1817,17 @@ function wp_widget_rss_form( $args, $inputs = null ) { * @param array $widget_rss RSS widget feed data. Expects unescaped data. * @param bool $check_feed Optional. Whether to check feed for errors. Default true. * @return array + * + * @phpstan-return array{ + * title: string, + * url: string, + * link: string, + * items: int<1, 20>, + * error: string|false, + * show_summary: int, + * show_author: int, + * show_date: int, + * } */ function wp_widget_rss_process( $widget_rss, $check_feed = true ) { $items = (int) $widget_rss['items']; diff --git a/tests/phpstan/baselines/argument.type.neon b/tests/phpstan/baselines/argument.type.neon index 730bd359d4fd3..83ad8d9610581 100644 --- a/tests/phpstan/baselines/argument.type.neon +++ b/tests/phpstan/baselines/argument.type.neon @@ -839,7 +839,7 @@ parameters: count: 1 path: ../../../src/wp-includes/class-wp-user.php - - message: '#^Parameter \#1 \$number of method WP_Widget\:\:_set\(\) expects int, string given\.$#' + message: '#^Parameter \#1 \$number of method WP_Widget\\>\:\:_set\(\) expects int, string given\.$#' identifier: argument.type count: 1 path: ../../../src/wp-includes/class-wp-widget.php diff --git a/tests/phpstan/baselines/isset.property.neon b/tests/phpstan/baselines/isset.property.neon index f5e272b598f34..fa7b3d126e71d 100644 --- a/tests/phpstan/baselines/isset.property.neon +++ b/tests/phpstan/baselines/isset.property.neon @@ -144,7 +144,7 @@ parameters: count: 1 path: ../../../src/wp-includes/class-wp-user.php - - message: '#^Property WP_Widget\:\:\$alt_option_name \(string\) in isset\(\) is not nullable\.$#' + message: '#^Property WP_Widget\\>\:\:\$alt_option_name \(string\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: ../../../src/wp-includes/class-wp-widget.php From c503b0381e0f422bc3a0cf419ff766b7b2c0acf6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:54:56 +0000 Subject: [PATCH 14/24] Themes: Add the PHPStan types the stubs carry for WP_Theme and the template tags. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `WP_Theme`'s magic properties are declared, and the headers its `ArrayAccess` implementation answers for become a named type, so reading an undefined offset resolves to null rather than to mixed. The map states `single_month_title()` as returning void in its display branch, where the function returns nothing at all on one path; the type says null, which is what a caller receives. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- phpstan.neon.dist | 1 - .../includes/class-custom-background.php | 7 ++++ .../includes/class-custom-image-header.php | 15 ++++++++ .../class-wp-theme-json-resolver.php | 2 ++ src/wp-includes/class-wp-theme.php | 20 +++++++++++ src/wp-includes/general-template.php | 8 +++++ .../baselines/parameter.defaultValue.neon | 35 ------------------- .../phpstan/baselines/property.notFound.neon | 30 ---------------- tests/phpstan/baselines/property.private.neon | 10 ------ 9 files changed, 52 insertions(+), 76 deletions(-) delete mode 100644 tests/phpstan/baselines/parameter.defaultValue.neon diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 98baf54d1d07d..b1240bc16b1f7 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -61,7 +61,6 @@ includes: - tests/phpstan/baselines/offsetAccess.nonOffsetAccessible.neon - tests/phpstan/baselines/offsetAccess.notFound.neon - tests/phpstan/baselines/offsetAssign.valueType.neon - - tests/phpstan/baselines/parameter.defaultValue.neon - tests/phpstan/baselines/parameter.notFound.neon - tests/phpstan/baselines/parameter.phpDocType.neon - tests/phpstan/baselines/parameterByRef.type.neon diff --git a/src/wp-admin/includes/class-custom-background.php b/src/wp-admin/includes/class-custom-background.php index 7f8f23485086a..7e318658da48b 100644 --- a/src/wp-admin/includes/class-custom-background.php +++ b/src/wp-admin/includes/class-custom-background.php @@ -19,6 +19,8 @@ class Custom_Background { * * @since 3.0.0 * @var callable + * + * @phpstan-var ''|callable(): void */ public $admin_header_callback; @@ -27,6 +29,8 @@ class Custom_Background { * * @since 3.0.0 * @var callable + * + * @phpstan-var ''|callable(): void */ public $admin_image_div_callback; @@ -47,6 +51,9 @@ class Custom_Background { * Default empty string. * @param callable $admin_image_div_callback Optional. Custom image div output callback. * Default empty string. + * + * @phpstan-param ''|callable(): void $admin_header_callback + * @phpstan-param ''|callable(): void $admin_image_div_callback */ public function __construct( $admin_header_callback = '', $admin_image_div_callback = '' ) { $this->admin_header_callback = $admin_header_callback; diff --git a/src/wp-admin/includes/class-custom-image-header.php b/src/wp-admin/includes/class-custom-image-header.php index c1816ffae2d9c..de0e8932714f7 100644 --- a/src/wp-admin/includes/class-custom-image-header.php +++ b/src/wp-admin/includes/class-custom-image-header.php @@ -19,6 +19,8 @@ class Custom_Image_Header { * * @since 2.1.0 * @var callable + * + * @phpstan-var ''|callable(): void */ public $admin_header_callback; @@ -27,6 +29,8 @@ class Custom_Image_Header { * * @since 3.0.0 * @var callable + * + * @phpstan-var ''|callable(): void */ public $admin_image_div_callback; @@ -54,6 +58,8 @@ class Custom_Image_Header { * @param callable $admin_header_callback Administration header callback. * @param callable $admin_image_div_callback Optional. Custom image div output callback. * Default empty string. + * + * @phpstan-param ''|callable(): void $admin_image_div_callback */ public function __construct( $admin_header_callback, $admin_image_div_callback = '' ) { $this->admin_header_callback = $admin_header_callback; @@ -310,6 +316,8 @@ public function process_default_headers() { * * @param string $type The header type. One of 'default' (for the Uploaded Images control) * or 'uploaded' (for the Uploaded Images control). + * + * @phpstan-param 'default'|'uploaded' $type */ public function show_header_selector( $type = 'default' ) { if ( 'default' === $type ) { @@ -1169,6 +1177,13 @@ public function filter_upload_tabs( $tabs ) { * registered for that theme; and the key of an image uploaded for that theme * (the attachment ID of the image). Or an array of arguments: attachment_id, * url, width, height. All are required. + * + * @phpstan-param string|array{ + * attachment_id: int<1, max>, + * url: string, + * width: int<0, max>, + * height: int<0, max>, + * } $choice */ final public function set_header_image( $choice ) { if ( is_array( $choice ) || is_object( $choice ) ) { diff --git a/src/wp-includes/class-wp-theme-json-resolver.php b/src/wp-includes/class-wp-theme-json-resolver.php index dc236f87907bb..05eedbcd40397 100644 --- a/src/wp-includes/class-wp-theme-json-resolver.php +++ b/src/wp-includes/class-wp-theme-json-resolver.php @@ -240,6 +240,8 @@ protected static function has_same_registered_blocks( $origin ) { * @type bool $with_supports Whether to include theme supports in the data. Default true. * } * @return WP_Theme_JSON Entity that holds theme data. + * + * @phpstan-param array{} $deprecated */ public static function get_theme_data( $deprecated = array(), $options = array() ) { if ( ! empty( $deprecated ) ) { diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php index 87399e399a198..2dfd6188f5ed3 100644 --- a/src/wp-includes/class-wp-theme.php +++ b/src/wp-includes/class-wp-theme.php @@ -5,6 +5,22 @@ * @package WordPress * @subpackage Theme * @since 3.4.0 + * + * @phpstan-type ThemeKey 'Name'|'Version'|'Status'|'Title'|'Author'|'Author Name'|'Author URI'|'Description'|'Template'|'Stylesheet'|'Template Files'|'Stylesheet Files'|'Template Dir'|'Stylesheet Dir'|'Screenshot'|'Tags'|'Theme Root'|'Theme Root URI'|'Parent Theme' + * @phpstan-property-read string $name + * @phpstan-property-read string $title + * @phpstan-property-read string $version + * @phpstan-property-read string $parent_theme + * @phpstan-property-read string $template_dir + * @phpstan-property-read string $stylesheet_dir + * @phpstan-property-read string $template + * @phpstan-property-read string $stylesheet + * @phpstan-property-read string $screenshot + * @phpstan-property-read string $description + * @phpstan-property-read string $author + * @phpstan-property-read list $tags + * @phpstan-property-read string $theme_root + * @phpstan-property-read string $theme_root_uri */ #[AllowDynamicProperties] final class WP_Theme implements ArrayAccess { @@ -654,6 +670,8 @@ public function offsetUnset( $offset ) {} * * @param mixed $offset * @return bool + * + * @phpstan-return ($offset is ThemeKey ? true : false) */ #[ReturnTypeWillChange] public function offsetExists( $offset ) { @@ -696,6 +714,8 @@ public function offsetExists( $offset ) { * * @param mixed $offset * @return mixed + * + * @phpstan-return ($offset is ThemeKey ? mixed : null) */ #[ReturnTypeWillChange] public function offsetGet( $offset ) { diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 0e35894fd47c4..324297eb8794d 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -1929,6 +1929,8 @@ function single_term_title( $prefix = '', $display = true ) { * @param string $prefix Optional. What to display before the title. * @param bool $display Optional. Whether to display or retrieve title. Default true. * @return string|false|null False if there's no valid title for the month. Title when retrieving. + * + * @phpstan-return ( $display is true ? false|null : false|string ) */ function single_month_title( $prefix = '', $display = true ) { global $wp_locale; @@ -4941,6 +4943,12 @@ function language_attributes( $doctype = 'html' ) { * } * @return string|string[]|null String of page links or array of page links, depending on 'type' argument. * Null if total number of pages is less than 2. + * + * @phpstan-return ( + * $args is array{total: int, ...} + * ? null + * : ($args is array{type: 'array', ...} ? list : string) + * ) */ function paginate_links( $args = '' ) { global $wp_query, $wp_rewrite; diff --git a/tests/phpstan/baselines/parameter.defaultValue.neon b/tests/phpstan/baselines/parameter.defaultValue.neon deleted file mode 100644 index 15f49b79a9ff0..0000000000000 --- a/tests/phpstan/baselines/parameter.defaultValue.neon +++ /dev/null @@ -1,35 +0,0 @@ -# PHPStan baseline for the `parameter.defaultValue` errors in WordPress core. -# -# https://phpstan.org/error-identifiers/parameter.defaultValue -# -# 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=parameter.defaultValue -# -# which reruns the analysis with this file suppressed so the errors surface again. - -parameters: - ignoreErrors: - - - message: '#^Default value of the parameter \#1 \$admin_header_callback \(''''\) of method Custom_Background\:\:__construct\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/class-custom-background.php - - - message: '#^Default value of the parameter \#2 \$admin_image_div_callback \(''''\) of method Custom_Background\:\:__construct\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/class-custom-background.php - - - message: '#^Default value of the parameter \#2 \$admin_image_div_callback \(''''\) of method Custom_Image_Header\:\:__construct\(\) is incompatible with type callable\(\)\: mixed\.$#' - identifier: parameter.defaultValue - count: 1 - path: ../../../src/wp-admin/includes/class-custom-image-header.php diff --git a/tests/phpstan/baselines/property.notFound.neon b/tests/phpstan/baselines/property.notFound.neon index 460a5082ab0b8..521bc7619766a 100644 --- a/tests/phpstan/baselines/property.notFound.neon +++ b/tests/phpstan/baselines/property.notFound.neon @@ -158,26 +158,6 @@ parameters: identifier: property.notFound count: 1 path: ../../../src/wp-admin/includes/class-walker-nav-menu-edit.php - - - message: '#^Access to an undefined property WP_Theme\:\:\$author\.$#' - identifier: property.notFound - count: 3 - path: ../../../src/wp-admin/includes/class-wp-debug-data.php - - - message: '#^Access to an undefined property WP_Theme\:\:\$name\.$#' - identifier: property.notFound - count: 4 - path: ../../../src/wp-admin/includes/class-wp-debug-data.php - - - message: '#^Access to an undefined property WP_Theme\:\:\$parent_theme\.$#' - identifier: property.notFound - count: 1 - path: ../../../src/wp-admin/includes/class-wp-debug-data.php - - - message: '#^Access to an undefined property WP_Theme\:\:\$version\.$#' - identifier: property.notFound - count: 6 - path: ../../../src/wp-admin/includes/class-wp-debug-data.php - message: '#^Access to an undefined property WP_Theme\:\:\$auto_update_forced\.$#' identifier: property.notFound @@ -188,11 +168,6 @@ parameters: identifier: property.notFound count: 1 path: ../../../src/wp-admin/includes/class-wp-ms-themes-list-table.php - - - message: '#^Access to an undefined property WP_Theme\:\:\$name\.$#' - identifier: property.notFound - count: 8 - path: ../../../src/wp-admin/includes/class-wp-site-health.php - message: '#^Access to an undefined property WP_Post\:\:\$_wp_attachment_image_alt\.$#' identifier: property.notFound @@ -228,11 +203,6 @@ parameters: identifier: property.notFound count: 2 path: ../../../src/wp-admin/nav-menus.php - - - message: '#^Access to an undefined property WP_Theme\:\:\$version\.$#' - identifier: property.notFound - count: 1 - path: ../../../src/wp-admin/update-core.php - message: '#^Access to an undefined property WP_Post\:\:\$description\.$#' identifier: property.notFound diff --git a/tests/phpstan/baselines/property.private.neon b/tests/phpstan/baselines/property.private.neon index 900a8e51dc423..7549ab9c5ddd5 100644 --- a/tests/phpstan/baselines/property.private.neon +++ b/tests/phpstan/baselines/property.private.neon @@ -18,16 +18,6 @@ parameters: ignoreErrors: - - - message: '#^Access to private property WP_Theme\:\:\$stylesheet\.$#' - identifier: property.private - count: 20 - path: ../../../src/wp-admin/includes/class-wp-debug-data.php - - - message: '#^Access to private property WP_Theme\:\:\$template\.$#' - identifier: property.private - count: 2 - path: ../../../src/wp-admin/includes/class-wp-debug-data.php - message: '#^Access to private property WP_Block_Type\:\:\$uses_context\.$#' identifier: property.private From 9eb64ce62617866bb8c981b5b37618f469af3b55 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 09:55:31 +0000 Subject: [PATCH 15/24] Script Loader: Add the PHPStan types the stubs carry for the script and style functions. php-stubs/wordpress-stubs, which every plugin's static analysis reads in place of core, applies a map of PHPStan types on top of core's docblocks when it generates those stubs. Each entry is a type core can carry itself, where it serves core's own analysis as well as the stubs generated from it. `wp_scripts_get_suffix()` returns one of two strings, the script and style tag builders take attributes whose values may be `true`, and `WP_Dependencies::query()` resolves to a `_WP_Dependency` for the two statuses that return one and to a bool for the rest. That last type is written without the template the map uses for it, which PHPStan reports as not being referenced by any parameter, and `wp_get_inline_script_tag()` does return an empty string when the processor rejects the script it is given. See #65817. Co-authored-by: Pascal Birchler Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013QJ9RphuttTR7cS5G4mU21 --- src/wp-includes/class-wp-dependencies.php | 14 ++++++++++++++ src/wp-includes/script-loader.php | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/src/wp-includes/class-wp-dependencies.php b/src/wp-includes/class-wp-dependencies.php index c2daba389bd75..4791b3f7b3255 100644 --- a/src/wp-includes/class-wp-dependencies.php +++ b/src/wp-includes/class-wp-dependencies.php @@ -74,6 +74,8 @@ class WP_Dependencies { * @since 2.8.0 * * @var (int|false)[] + * + * @phpstan-var array */ public $groups = array(); @@ -472,6 +474,16 @@ protected function recurse_deps( $queue, $handle ) { * @param string $handle Name of the item. Should be unique. * @param string $status Optional. Status of the item to query. Default 'registered'. * @return bool|_WP_Dependency Found, or object Item data. + * + * @phpstan-return ( + * $handle is not non-empty-string + * ? false + * : ( + * $status is not 'registered'|'scripts'|'enqueued'|'queued'|'to_do'|'to_print'|'done'|'printed' + * ? false + * : ( $status is 'registered'|'scripts' ? _WP_Dependency|false : bool ) + * ) + * ) */ public function query( $handle, $status = 'registered' ) { switch ( $status ) { @@ -529,6 +541,8 @@ public function set_group( $handle, $recursion, $group ) { * * @param string[] $load Array of script or style handles to load. * @return string Etag header. + * + * @phpstan-return non-falsy-string */ public function get_etag( $load ) { /* diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 7d5ba24e5617d..a8d8afe171b39 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -685,6 +685,8 @@ function wp_default_packages( $scripts ) { * * @param string $type The type of suffix to retrieve. * @return string The script suffix. + * + * @phpstan-return ''|'.min' */ function wp_scripts_get_suffix( $type = '' ) { static $suffixes; @@ -2938,6 +2940,9 @@ function wp_enqueue_editor_format_library_assets() { * * @param array $attributes Key-value pairs representing `