From eaddbe42214ee383faa881835f21ce3311b5d115 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 2 Sep 2026 15:24:25 -0700 Subject: [PATCH 1/6] Cover every falsy `$args` scalar in the undecidable branch The template tags whose display flag lives in an `$args` array also accept a query string, which PHPStan cannot read, so their conditional return types end in a third branch that leaves the type a union and reports nothing. That branch tested `$args is ''|array`, treating the empty string as the only scalar that behaves like an empty argument set. `'0'` behaves the same way. `parse_str( '0', $r )` yields `array( 0 => '' )`, so `wp_parse_args( '0', $defaults )` sets no key the tag reads and the flag keeps its default: the call prints and returns nothing, exactly as `''` does. It was resolving to the retrieval type instead, so consuming its meaningless result went unreported. Widening the branch to `''|'0'|array` closes that, and matches the flag conditions themselves, which already spell the falsy set out as `false|0|''|'0'`. Ten tags are affected: `wp_list_authors()`, `wp_list_bookmarks()`, `wp_tag_cloud()`, `wp_list_comments()`, `paginate_comments_links()`, `wp_get_archives()`, `the_title_attribute()`, `wp_list_pages()`, `wp_page_menu()` and `wp_list_users()`. Every other call shape is unchanged. A query string carrying a visible flag, and an `$args` built at runtime, both stay unions and are still reported in neither mode; an array setting the flag still resolves to the retrieval type; an empty array or a truthy flag still resolves to `void`. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/author-template.php | 2 +- src/wp-includes/bookmark-template.php | 2 +- src/wp-includes/category-template.php | 2 +- src/wp-includes/comment-template.php | 2 +- src/wp-includes/general-template.php | 2 +- src/wp-includes/link-template.php | 2 +- src/wp-includes/post-template.php | 6 +++--- src/wp-includes/user.php | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/author-template.php b/src/wp-includes/author-template.php index 77b1adaf96ac0..b27bbf62379d4 100644 --- a/src/wp-includes/author-template.php +++ b/src/wp-includes/author-template.php @@ -453,7 +453,7 @@ function get_author_posts_url( $author_id, $author_nicename = '' ) { * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string - * : ( $args is ''|array ? void : string|null ) + * : ( $args is ''|'0'|array ? void : string|null ) * ) */ function wp_list_authors( $args = '' ) { diff --git a/src/wp-includes/bookmark-template.php b/src/wp-includes/bookmark-template.php index b3f94decef2d2..37d55bf4561f6 100644 --- a/src/wp-includes/bookmark-template.php +++ b/src/wp-includes/bookmark-template.php @@ -210,7 +210,7 @@ function _walk_bookmarks( $bookmarks, $args = '' ) { * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string - * : ( $args is ''|array ? void : string|null ) + * : ( $args is ''|'0'|array ? void : string|null ) * ) */ function wp_list_bookmarks( $args = '' ) { diff --git a/src/wp-includes/category-template.php b/src/wp-includes/category-template.php index 633c8faf81c51..8778a5b9902aa 100644 --- a/src/wp-includes/category-template.php +++ b/src/wp-includes/category-template.php @@ -718,7 +718,7 @@ function wp_list_categories( $args = '' ) { * ? string[]|null * : ( $args is array{ echo: false|0|''|'0', ... } * ? string|null - * : ( $args is ''|array ? void : string|string[]|null ) ) + * : ( $args is ''|'0'|array ? void : string|string[]|null ) ) * ) */ function wp_tag_cloud( $args = '' ) { diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index 68091c5f4c65e..f88f9985e515c 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -2238,7 +2238,7 @@ function _get_comment_reply_id( $post = null ) { * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string|null - * : ( $args is ''|array ? void : string|null ) + * : ( $args is ''|'0'|array ? void : string|null ) * ) */ function wp_list_comments( $args = array(), $comments = null ) { diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index e5c65279e52b5..a322eeac26e6d 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -2240,7 +2240,7 @@ function get_archives_link( $url, $text, $format = 'html', $before = '', $after * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string|null - * : ( $args is ''|array ? void : string|null ) + * : ( $args is ''|'0'|array ? void : string|null ) * ) */ function wp_get_archives( $args = '' ) { diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index cabb7cedfab21..86f60ba858916 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -3277,7 +3277,7 @@ function previous_comments_link( $label = '' ) { * ? string[]|null * : ( $args is array{ echo: false|0|''|'0', ... } * ? string|null - * : ( $args is ''|array ? void : string|string[]|null ) ) + * : ( $args is ''|'0'|array ? void : string|string[]|null ) ) * ) */ function paginate_comments_links( $args = array() ) { diff --git a/src/wp-includes/post-template.php b/src/wp-includes/post-template.php index 0c21f6815a28b..1962f2570ab69 100644 --- a/src/wp-includes/post-template.php +++ b/src/wp-includes/post-template.php @@ -82,7 +82,7 @@ function the_title( $before = '', $after = '', $display = true ) { * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string|null - * : ( $args is ''|array ? void : string|null ) + * : ( $args is ''|'0'|array ? void : string|null ) * ) */ function the_title_attribute( $args = '' ) { @@ -1309,7 +1309,7 @@ function wp_dropdown_pages( $args = '' ) { * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string - * : ( $args is ''|array ? void : string|null ) + * : ( $args is ''|'0'|array ? void : string|null ) * ) */ function wp_list_pages( $args = '' ) { @@ -1437,7 +1437,7 @@ function wp_list_pages( $args = '' ) { * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string - * : ( $args is ''|array ? void : string|null ) + * : ( $args is ''|'0'|array ? void : string|null ) * ) */ function wp_page_menu( $args = array() ) { diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 0a0aed8348073..3e8d76b1e4a3b 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -911,7 +911,7 @@ function get_users( $args = array() ) { * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string - * : ( $args is ''|array ? void : string|null ) + * : ( $args is ''|'0'|array ? void : string|null ) * ) */ function wp_list_users( $args = array() ) { From 32d8d7c489a45fa778c053d6695ae1385dc94079 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 2 Sep 2026 15:35:37 -0700 Subject: [PATCH 2/6] Correct the `wp_dropdown_languages()` bail description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The return description said nothing comes back "when the required `id` or `name` argument is missing". Neither is required and neither can go missing: both default to `'locale'`, so a caller omitting them gets a dropdown. What the function actually bails on is a falsy value the caller supplied for one of them. The `void` in the union covers exactly that bail, and until now it did nothing — a plain union never resolves to `void`, so the annotation was silent for every call. A conditional makes it report where the bail is visible: `''` and `'0'` are the falsy strings the arguments are documented to hold, mirroring how the sibling tags spell the falsy set of a `bool|int` flag as `false|0|''|'0'`. The two shapes have to nest rather than combine. PHPStan cannot represent a union of two unsealed array shapes with different required keys, and widens `array{ id: ''|'0', ... }|array{ name: ''|'0', ... }` to plain `non-empty-array` — which would ask whether `$args` has any keys at all, and so resolve every populated array, including all seven call sites in core, to `void`. Nested, each shape is tested on its own. A call that supplies both arguments, sets neither, passes a query string, or builds `$args` at runtime is unaffected and reported in neither mode. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/l10n.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index a0be55146f7e6..1190a79b35c39 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -1736,8 +1736,13 @@ function wp_get_l10n_php_file_data( $php_file ) { * instead of an empty value. Default false. * } * @return string|void HTML dropdown list of languages. Always returned, whether or not - * 'echo' is true; nothing is returned when the required `id` or `name` - * argument is missing. + * 'echo' is true; nothing is returned when the 'id' or 'name' + * argument is empty. + * @phpstan-return ( + * $args is array{ id: ''|'0', ... } + * ? void + * : ( $args is array{ name: ''|'0', ... } ? void : string ) + * ) */ function wp_dropdown_languages( $args = array() ) { From 7dd8325f29eeec5cf635f985de818c733de973da Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 2 Sep 2026 15:38:04 -0700 Subject: [PATCH 3/6] Say what `the_date()` and `the_modified_date()` return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both described their return as "String if retrieving." — naming the type rather than the content, and saying nothing at all about display mode, which is the one outcome the `void` in the union exists to convey. Every other tag of this shape states both halves. `the_date()` now names the date and the case where there isn't one: the function builds its value only when `is_new_day()`, so a post sharing a date with the one before it retrieves an empty string. That is the whole point of the function, and naming the content without the caveat would promise a date the caller may not get — the same correction already applied to `next_posts()`, `previous_posts()` and `wp_register()`. `the_modified_date()` always concatenates a date, so it takes the short form. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/general-template.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index a322eeac26e6d..e584e78fed515 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -2927,7 +2927,8 @@ function the_date_xml() { * @param string $before Optional. Output before the date. Default empty. * @param string $after Optional. Output after the date. Default empty. * @param bool $display Optional. Whether to echo the date or return it. Default true. - * @return string|void String if retrieving. + * @return string|void The date when `$display` is false, or an empty string when the post's + * date matches the previously output one. Nothing otherwise. * @phpstan-return ( $display is true ? void : string ) */ function the_date( $format = '', $before = '', $after = '', $display = true ) { @@ -3003,7 +3004,7 @@ function get_the_date( $format = '', $post = null ) { * @param string $before Optional. Output before the date. Default empty. * @param string $after Optional. Output after the date. Default empty. * @param bool $display Optional. Whether to echo the date or return it. Default true. - * @return string|void String if retrieving. + * @return string|void The modified date when `$display` is false, nothing otherwise. * @phpstan-return ( $display is true ? void : string ) */ function the_modified_date( $format = '', $before = '', $after = '', $display = true ) { From 20c7f9d0c8dfec42f1f669db005cf241504c1c92 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 2 Sep 2026 15:44:24 -0700 Subject: [PATCH 4/6] Restore `void` on two `wpdb` methods that never return a value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `print_error()` and `check_database_version()` documented `void|false` and `void|WP_Error` until r62177 replaced the `void` with `null` and added a trailing `return null;` to each. The premise was that `void` cannot belong to a union, which holds for PHP's native return types but not for PHPDoc. Both are the shape that keeps `void` elsewhere in core: the method either succeeds, with nothing to hand back, or reports a failure. `print_error()` returns `false` only when errors are suppressed or hidden; otherwise it prints and has no value to give. `check_database_version()` returns a `WP_Error` only when the server is too old, and its one caller, `wp_check_mysql_version()`, tests `is_wp_error()` and ignores the rest. Under `null` that meaningless value read as a legitimate one, which is what the `void` was there to deny — and what nineteen sibling functions documenting `void|false`, and seven documenting `void|WP_Error`, still say. Neither can carry a conditional return type: both switch on object state rather than on an argument, so the distinction is for the reader, not the analyser. Removing the explicit `return null;` changes nothing at runtime, since falling off the end returns `null` anyway, and `void` in the union is what licenses it. The rest of r62177 stands. `prepare()` and `get_row()` return a `null` their callers genuinely consume, `check_connection()` and `bail()` were corrected in the other direction because their old `void` branches die rather than return, and `get_col_info()` is documented `mixed`. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/class-wpdb.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index a676d395ee95c..5cf9508f8fbdf 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -1794,7 +1794,7 @@ public function esc_like( $text ) { * @global array $EZSQL_ERROR Stores error information of query and error string. * * @param string $str The error to display. - * @return null|false Null if the showing of errors is enabled, false if disabled. + * @return void|false Void if the showing of errors is enabled, false if disabled. */ public function print_error( $str = '' ) { global $EZSQL_ERROR; @@ -1855,8 +1855,6 @@ public function print_error( $str = '' ) { $query ); } - - return null; } /** @@ -4068,7 +4066,7 @@ public function close() { * @since 2.5.0 * * @global string $required_mysql_version The minimum required MySQL version string. - * @return WP_Error|null + * @return void|WP_Error Void if the server meets the minimum version, WP_Error if not. */ public function check_database_version() { global $required_mysql_version; @@ -4079,8 +4077,6 @@ public function check_database_version() { /* translators: 1: WordPress version number, 2: Minimum required MySQL version number. */ return new WP_Error( 'database_version', sprintf( __( 'Error: WordPress %1$s requires MySQL %2$s or higher' ), $wp_version, $required_mysql_version ) ); } - - return null; } /** From 96a52a765e3668de7846bef3ac335a6b7e898c0e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 2 Sep 2026 16:00:24 -0700 Subject: [PATCH 5/6] Name `null` in the tags whose retrieval branch can return it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifteen dual-mode tags document a `null` in their return description while the `@return` tag itself lists only `string|void`, so the tag omits a value the prose promises and the conditional `@phpstan-return` already states. The three-way union was rejected when these annotations were written, on the grounds that no such form appeared elsewhere in core. That was wrong. Immediately before r62177 the tree held three — `WP_Block_Type::__get()` as `string|string[]|null|void`, `wpdb::get_row()` as `array|object|null|void` and one in `WP_Theme_JSON` as `null|void`. All three were removed by the same campaign that replaced `void` in unions on the premise that a union cannot hold it, which is the premise r63441 corrected. The idiom existed; it was erased by the mistake being undone here. Thirteen tags become `string|null|void` and two, `wp_tag_cloud()` and `paginate_comments_links()`, become `string|string[]|null|void`. Values come first, then `null`, then `void`, following the form the tree used before. The test is the conditional's retrieval branch, not the wording: `null` is named only where a caller can observe it. `wp_list_pages()`, `wp_page_menu()`, `wp_list_authors()`, `wp_list_bookmarks()` and `wp_list_users()` look like the same shape but are left alone — their `null` sits in the undecidable branch that covers a query string or an `$args` built at runtime, which is the union of both modes rather than a value retrieval mode can hand back, and their retrieval branch is plain `string`. Co-Authored-By: Claude Opus 5 (1M context) --- .../themes/twentytwenty/inc/template-tags.php | 4 +-- src/wp-includes/category-template.php | 6 ++-- src/wp-includes/comment-template.php | 4 +-- src/wp-includes/functions.php | 4 +-- src/wp-includes/general-template.php | 28 +++++++++---------- src/wp-includes/link-template.php | 12 ++++---- src/wp-includes/post-template.php | 8 +++--- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/wp-content/themes/twentytwenty/inc/template-tags.php b/src/wp-content/themes/twentytwenty/inc/template-tags.php index 28eaad2b4e42d..7987e4c57af3f 100644 --- a/src/wp-content/themes/twentytwenty/inc/template-tags.php +++ b/src/wp-content/themes/twentytwenty/inc/template-tags.php @@ -107,8 +107,8 @@ function twentytwenty_site_logo( $args = array(), $display = true ) { * @since Twenty Twenty 1.0 * * @param bool $display Display or return the HTML. - * @return string|void The HTML when `$display` is false, null when the site has no - * description. Nothing otherwise. + * @return string|null|void The HTML when `$display` is false, null when the site has no + * description. Nothing otherwise. * @phpstan-return ( $display is true ? void : string|null ) */ function twentytwenty_site_description( $display = true ) { diff --git a/src/wp-includes/category-template.php b/src/wp-includes/category-template.php index 8778a5b9902aa..f268f93cbc461 100644 --- a/src/wp-includes/category-template.php +++ b/src/wp-includes/category-template.php @@ -710,9 +710,9 @@ function wp_list_categories( $args = '' ) { * associated with the taxonomy. * @type bool $echo Whether or not to echo the return value. Default true. * } - * @return string|string[]|void Tag cloud as a string, or as an array when the 'format' argument - * is 'array'. Null on failure. Nothing when 'echo' is true and - * 'format' is not 'array'. + * @return string|string[]|null|void Tag cloud as a string, or as an array when the 'format' + * argument is 'array'. Null on failure. Nothing when 'echo' is + * true and 'format' is not 'array'. * @phpstan-return ( * $args is array{ format: 'array', ... } * ? string[]|null diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index f88f9985e515c..123bbfc060189 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -2233,8 +2233,8 @@ function _get_comment_reply_id( $post = null ) { * @type bool $echo Whether to echo the output or return it. Default true. * } * @param WP_Comment[] $comments Optional. Array of WP_Comment objects. Default null. - * @return string|void HTML list of comments when 'echo' is false, null when there are no - * comments to list. Nothing otherwise. + * @return string|null|void HTML list of comments when 'echo' is false, null when there are + * no comments to list. Nothing otherwise. * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string|null diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 5b59d393213ac..bf0001c28e51c 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -8737,8 +8737,8 @@ function wp_get_default_update_php_url() { * @param string $before Markup to output before the annotation. Default `

`. * @param string $after Markup to output after the annotation. Default `

`. * @param bool $display Whether to echo or return the markup. Default `true` for echo. - * @return string|void Update PHP page annotation when `$display` is false, null when no - * annotation is available. Nothing otherwise. + * @return string|null|void Update PHP page annotation when `$display` is false, null when + * no annotation is available. Nothing otherwise. * @phpstan-return ( $display is true ? void : string|null ) */ function wp_update_php_annotation( $before = '

', $after = '

', $display = true ) { diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index e584e78fed515..7ba1d623f4631 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -1705,8 +1705,8 @@ function wp_title( $sep = '»', $display = true, $seplocation = '' ) { * * @param string $prefix Optional. What to display before the title. * @param bool $display Optional. Whether to display or retrieve title. Default true. - * @return string|void Title when retrieving, null on failure. - * Nothing when displaying. + * @return string|null|void Title when retrieving, null on failure. + * Nothing when displaying. * @phpstan-return ( $display is true ? void : string|null ) */ function single_post_title( $prefix = '', $display = true ) { @@ -1743,8 +1743,8 @@ function single_post_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|void Title when retrieving, null on failure. - * Nothing when displaying. + * @return string|null|void Title when retrieving, null on failure. + * Nothing when displaying. * @phpstan-return ( $display is true ? void : string|null ) */ function post_type_archive_title( $prefix = '', $display = true ) { @@ -1787,8 +1787,8 @@ function post_type_archive_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|void Title when retrieving, null on failure. - * Nothing when displaying. + * @return string|null|void Title when retrieving, null on failure. + * Nothing when displaying. * @phpstan-return ( $display is true ? void : string|null ) */ function single_cat_title( $prefix = '', $display = true ) { @@ -1810,8 +1810,8 @@ function single_cat_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|void Title when retrieving, null on failure. - * Nothing when displaying. + * @return string|null|void Title when retrieving, null on failure. + * Nothing when displaying. * @phpstan-return ( $display is true ? void : string|null ) */ function single_tag_title( $prefix = '', $display = true ) { @@ -1833,8 +1833,8 @@ function single_tag_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|void Title when retrieving, null on failure. - * Nothing when displaying. + * @return string|null|void Title when retrieving, null on failure. + * Nothing when displaying. * @phpstan-return ( $display is true ? void : string|null ) */ function single_term_title( $prefix = '', $display = true ) { @@ -2235,8 +2235,8 @@ function get_archives_link( $url, $text, $format = 'html', $before = '', $after * @type string $day Day. Default current day. * @type string $w Week. Default current week. * } - * @return string|void Archive links when 'echo' is false, null when the post type is - * not viewable. Nothing otherwise. + * @return string|null|void Archive links when 'echo' is false, null when the post type is + * not viewable. Nothing otherwise. * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string|null @@ -2511,8 +2511,8 @@ function calendar_week_mod( $num ) { * @type bool $display Whether to display the calendar output. Default true. * @type string $post_type Optional. Post type. Default 'post'. * } - * @return string|void Calendar HTML when `$display` is false, null when the site has - * no posts. Nothing otherwise. + * @return string|null|void Calendar HTML when `$display` is false, null when the site has + * no posts. Nothing otherwise. * @phpstan-return ( $args is array{ display: false|0|''|'0', ... } ? string|null : void ) */ function get_calendar( $args = array() ) { diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index 86f60ba858916..f4accadb0bf3a 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -1131,8 +1131,8 @@ function get_edit_term_link( $term, $taxonomy = '', $object_type = '' ) { * @param string $after Optional. Display after edit link. Default empty. * @param int|WP_Term|null $term Optional. Term ID or object. If null, the queried object will be inspected. Default null. * @param bool $display Optional. Whether or not to echo the return. Default true. - * @return string|void HTML content when retrieving, null on failure or without the - * capability to edit the term. Nothing when displaying. + * @return string|null|void HTML content when retrieving, null on failure or without the + * capability to edit the term. Nothing when displaying. * @phpstan-return ( $display is true ? void : string|null ) */ function edit_term_link( $link = '', $before = '', $after = '', $term = null, $display = true ) { @@ -3268,10 +3268,10 @@ function previous_comments_link( $label = '' ) { * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string|array $args Optional args. See paginate_links(). Default empty array. - * @return string|string[]|void Markup for comment page links, or an array of them when the 'type' - * argument is 'array'. Null if the query is not for an existing single - * post of any post type. Nothing when 'echo' is true and 'type' is not - * 'array'. + * @return string|string[]|null|void Markup for comment page links, or an array of them when + * the 'type' argument is 'array'. Null if the query is not for + * an existing single post of any post type. Nothing when 'echo' + * is true and 'type' is not 'array'. * @phpstan-return ( * $args is array{ type: 'array', ... } * ? string[]|null diff --git a/src/wp-includes/post-template.php b/src/wp-includes/post-template.php index 1962f2570ab69..beb53c92b0c46 100644 --- a/src/wp-includes/post-template.php +++ b/src/wp-includes/post-template.php @@ -37,8 +37,8 @@ function get_the_ID() { // phpcs:ignore WordPress.NamingConventions.ValidFunctio * @param string $before Optional. Markup to prepend to the title. Default empty. * @param string $after Optional. Markup to append to the title. Default empty. * @param bool $display Optional. Whether to echo or return the title. Default true for echo. - * @return string|void Current post title when `$display` is false, null when the title - * is empty. Nothing otherwise. + * @return string|null|void Current post title when `$display` is false, null when the + * title is empty. Nothing otherwise. * @phpstan-return ( $display is true ? void : string|null ) */ function the_title( $before = '', $after = '', $display = true ) { @@ -77,8 +77,8 @@ function the_title( $before = '', $after = '', $display = true ) { * @type bool $echo Whether to echo or return the title. Default true for echo. * @type WP_Post $post Current post object to retrieve the title for. * } - * @return string|void The title attribute when 'echo' is false, null when the title is - * empty. Nothing otherwise. + * @return string|null|void The title attribute when 'echo' is false, null when the title + * is empty. Nothing otherwise. * @phpstan-return ( * $args is array{ echo: false|0|''|'0', ... } * ? string|null From c8fdc2b036f135c30398e7c2bfd4591ee200598f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 3 Sep 2026 12:42:59 -0700 Subject: [PATCH 6/6] Flag the deprecated `trackback_url()` argument through its type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `$deprecated_echo` argument has been deprecated since 2.5.0, and passing `false` still triggers `_deprecated_argument()` at runtime, but nothing said so statically. Typing the parameter `true` reports such a call as `Parameter #1 $deprecated_echo of function trackback_url expects true, false given.`, which is the convention php-stubs/wordpress-stubs uses to signal a deprecated argument. Declaring it that way makes the conditional return type's condition constant, so PHPStan reports `conditionalType.alwaysTrue` on the signature. The conditional still earns its place — a call passing `false` has its return resolved from the argument's own type and so types as `string`, while `trackback_url()` resolves to `void` and is reported when consumed — so the condition is suppressed rather than removed. The ignore sits in the docblock because that is where the error is raised: the rule is about the declared return type, a property of the signature, not of any statement in the body. It is line-scoped and identifier-scoped, covering only that one error on the declaration, and PHPStan reports an unmatched ignore if the parameter is ever widened again, so it cannot rot into dead suppression. No call site in `src/` or `tests/` passes the argument. Co-Authored-By: IanDelMar <42134098+IanDelMar@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/comment-template.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index 123bbfc060189..f96cf18f78f13 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -1244,10 +1244,12 @@ function get_trackback_url() { * * @see get_trackback_url() * - * @param bool $deprecated_echo Deprecated. Use {@see get_trackback_url()}. Echo the URL or + * @param true $deprecated_echo Deprecated. Use {@see get_trackback_url()}. Echo the URL or * return it. Default true. * @return string|void The trackback URL when `$deprecated_echo` is false, nothing otherwise. * @phpstan-return ( $deprecated_echo is true ? void : string ) + * + * @phpstan-ignore conditionalType.alwaysTrue (Typed `true` to flag the deprecated argument.) */ function trackback_url( $deprecated_echo = true ) { if ( true !== $deprecated_echo ) {