From 6781cdd5e90d5283c0117706346b4afaf6e550b5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 28 Aug 2026 11:30:17 +0000 Subject: [PATCH 1/6] Docs: Add a conditional return type to `get_post_stati()`. What `get_post_stati()` returns is decided entirely by its `$output` argument: `'names'` plucks the `name` field off every registered status object, anything else hands back the objects themselves. The plain `@return string[]|stdClass[]` cannot express that, so every call site is handed a union whose other half can never occur for that call, and everything done with the result -- `array_diff()`, `array_filter()`, `in_array()`, `array_fill_keys()`, `foreach` -- is analysed against `string|stdClass` values rather than the one type actually present. A `@phpstan-return` conditional on `$output` resolves each call to the branch it can really return. This is the annotation `get_post_types()` already carries a hundred lines below, and the two are the same function in different clothes: each reads a registry global and delegates to `wp_filter_object_list()` with `'name'` or `false` as the field, and each documents a `string[]` or object union. Matching annotations keep the pair readable side by side. The human-readable `@return` is unchanged. The key type is deliberately left unconstrained. `WP_List_Util::filter()` and `WP_List_Util::pluck()` both preserve the keys of their input, so the result is keyed exactly as `$wp_post_statuses` is, and core documents that global as `stdClass[]`, whose key type is `array-key`. Status names go through `sanitize_key()`, which permits digits, and PHP stores a numeric string key as an integer, so narrowing the key to `string` would claim more than the code guarantees. `string[]` and `stdClass[]` assert exactly what the existing `@return` already asserts, and no more. No error is resolved by this change on its own, and no baseline entry changes. It is a prerequisite: call sites that pass the result onwards cannot be typed correctly while the callee advertises a union it never returns. See #65817. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy --- src/wp-includes/post.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 49ab472d37184..3cde0bfe58936 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -1575,6 +1575,7 @@ function get_post_status_object( $post_status ) { * from the array needs to match; 'and' means all elements must match. * Default 'and'. * @return string[]|stdClass[] A list of post status names or objects. + * @phpstan-return ( $output is 'names' ? string[] : stdClass[] ) */ function get_post_stati( $args = array(), $output = 'names', $operator = 'and' ) { global $wp_post_statuses; From ebac5fe0f7e39775e6d656f237b848c52c23e321 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 28 Aug 2026 12:20:58 +0000 Subject: [PATCH 2/6] Docs: Add a conditional return type to `get_taxonomies()`. `get_taxonomies()` is the same function as `get_post_stati()` and `get_post_types()` once more: it reads a registry global and returns `wp_filter_object_list( $wp_taxonomies, $args, $operator, $field )` with `$field` set to `'name'` or `false` depending on `$output`. The plain `@return string[]|WP_Taxonomy[]` cannot express that dependency, so each of the 26 call sites in `src/` is handed a union whose other half can never occur for that call. The test is `$output is 'names'`, not `$output is 'objects'`, and here that is load-bearing rather than stylistic. Of the 26 call sites, two pass `'names'` explicitly, eight omit `$output` entirely, twelve pass `'objects'` -- and four pass the singular `'object'`: `wp-admin/includes/nav-menu.php`, `wp-admin/includes/ajax-actions.php`, `rest-api.php` and `widgets/class-wp-widget-tag-cloud.php`. All four reach the object branch, because the implementation asks only whether `$output` is `'names'`. Testing for `'objects'` would have typed those four as `string[]` and broken every property read on the result. The key type is left unconstrained, on the same evidence as `get_post_stati()`. `WP_List_Util::filter()` and `WP_List_Util::pluck()` both preserve their input's keys, so the result is keyed exactly as `$wp_taxonomies` is, and core documents that global as `WP_Taxonomy[]`, whose key type is `array-key`. Three call sites do read the key -- `WP::parse_request()` twice and `WP_Query::parse_tax_query()` once, all as `foreach ( ... as $taxonomy => $t )` -- and each uses it only in a `===` comparison or as an array value, so none of them needs the key narrowed to `string`. The human-readable `@return` is unchanged. See #65817. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy --- src/wp-includes/taxonomy.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 29317f0a8bf9b..6576826fe07d2 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -278,6 +278,7 @@ function create_initial_taxonomies() { * one element from the array needs to match; 'and' means all elements must match. * Default 'and'. * @return string[]|WP_Taxonomy[] An array of taxonomy names or objects. + * @phpstan-return ( $output is 'names' ? string[] : WP_Taxonomy[] ) */ function get_taxonomies( $args = array(), $output = 'names', $operator = 'and' ) { global $wp_taxonomies; From 884d7cdab6b3fb4a410abc08555778fdc77dbe4b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 28 Aug 2026 12:27:19 +0000 Subject: [PATCH 3/6] Docs: Add a conditional return type to `get_object_taxonomies()`. Like `get_taxonomies()` before it, `get_object_taxonomies()` returns names or objects according to `$output`, and its plain `@return string[]|WP_Taxonomy[]` hands each of its 50 call sites in `src/` a union whose other half can never occur. Unlike `get_taxonomies()`, it does not delegate to `wp_filter_object_list()`; it builds the array inline, and the two branches build different shapes: $taxonomies[] = $tax_name; // names $taxonomies[ $tax_name ] = $tax_obj; // objects So the names branch is worth more than `string[]`. It only ever appends, which means integer keys, and `array` says so where `string[]` leaves the key as `array-key`. It is deliberately not `list`, even though the loop alone would produce one. When `$object_type` is an attachment the function returns `get_attachment_taxonomies( $object_type, $output )` instead, whose names branch ends in `array_unique()`. `array_unique()` preserves keys rather than renumbering them, so that path returns `array, string>` and not a list. Claiming `list` here would be wrong for exactly the path that leaves this function's own loop untouched. The objects branch stays `WP_Taxonomy[]`. Its key is `$tax_name`, the key of `$wp_taxonomies`, and core documents that global as `WP_Taxonomy[]`, whose key type is `array-key`. Narrowing it to `string` would claim more than the code guarantees, the same judgement made for `get_post_stati()` and `get_taxonomies()`. The test is `$output is 'names'` for the reason it always is: two call sites in `post.php` pass the singular `'object'`, and only a test against `'names'` routes them to the object branch. The human-readable `@return` is unchanged. See #65817. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy --- src/wp-includes/taxonomy.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 6576826fe07d2..de6ee7598c74f 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -308,6 +308,7 @@ function get_taxonomies( $args = array(), $output = 'names', $operator = 'and' ) * @param string $output Optional. The type of output to return in the array. Accepts either * 'names' or 'objects'. Default 'names'. * @return string[]|WP_Taxonomy[] The names or objects of all taxonomies of `$object_type`. + * @phpstan-return ( $output is 'names' ? array : WP_Taxonomy[] ) */ function get_object_taxonomies( $object_type, $output = 'names' ) { global $wp_taxonomies; From 0ab5241b7beca517499bb4af20dc466d3be8d9f4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 28 Aug 2026 12:32:38 +0000 Subject: [PATCH 4/6] Docs: Add a conditional return type to `get_attachment_taxonomies()`. Third in the family after `get_taxonomies()` and `get_object_taxonomies()`, and the one whose body least resembles the others. It collects results from `get_object_taxonomies()` across several object types, merges them, and for the names case runs the merged array through `array_unique()`. The names branch is `array`. `array_merge()` renumbers integer keys, so the merged array is a list; `array_unique()` then preserves keys rather than renumbering them, so a duplicate removed from the middle leaves a gap and the result is no longer a list. That is why this branch, and `get_object_taxonomies()`'s alongside it, stop at `array` rather than claiming `list` -- the key is provably an integer, the sequence is not provably intact. The objects branch stays `WP_Taxonomy[]`. It merges what `get_object_taxonomies()` returns for objects, which is keyed by the keys of `$wp_taxonomies`, documented as `array-key`; `array_merge()` renumbers the integer ones and preserves the string ones, so nothing narrower is provable at the end of it. Both branches admit the early `return array();` taken when `$attachment` does not resolve to an object, since an empty array satisfies either. Worth noting for whoever reads this next: all four call sites in `src/` omit `$output` and so take the names branch. The objects branch is reachable only through `get_object_taxonomies()`, which passes `$output` through when it delegates an attachment. The annotation is still the right description of the function, and it matches its three siblings, but core does not presently exercise half of it. The human-readable `@return` is unchanged. See #65817. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy --- src/wp-includes/media.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 6719126ea4e23..880848c71b103 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -4160,6 +4160,7 @@ function adjacent_image_link( $prev = true, $size = 'thumbnail', $text = false ) * or 'objects' to return an array of taxonomy objects. * Default is 'names'. * @return string[]|WP_Taxonomy[] List of taxonomies or taxonomy names. Empty array on failure. + * @phpstan-return ( $output is 'names' ? array : WP_Taxonomy[] ) */ function get_attachment_taxonomies( $attachment, $output = 'names' ) { if ( is_int( $attachment ) ) { From 183be5c2d198292a7e05c1d3f584f73839de5d2b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 28 Aug 2026 12:36:58 +0000 Subject: [PATCH 5/6] Docs: Add a conditional return type to `get_taxonomies_for_attachments()`. Last of the family, and the only one whose two branches can both be described exactly. Where `get_object_taxonomies()` builds its result out of the keys of `$wp_taxonomies`, this function builds its result out of `WP_Taxonomy::$name`: $taxonomies[] = $taxonomy->name; // names $taxonomies[ $taxonomy->name ] = $taxonomy; // objects `$name` is documented `@var string` on `WP_Taxonomy`, so the value type of the names branch and the key type of the objects branch are both `string` on the class's own annotation rather than on an assumption about the registry global. That is what makes `array` honest here and `WP_Taxonomy[]` the right stopping point in the two functions before it: the difference is not style, it is where the key comes from. The names branch is `list`, not `array`. The loop appends and nothing else -- there is no `array_unique()` and no delegated path, the two things that cost `get_object_taxonomies()` and `get_attachment_taxonomies()` their list-ness. The objects source is `get_taxonomies( array(), 'objects' )`, which an earlier change in this series types as `WP_Taxonomy[]`, so `$taxonomy` is a `WP_Taxonomy` and `$taxonomy->name` resolves on it directly. Of the four call sites in `src/`, three pass `'objects'` and one omits `$output`. The human-readable `@return` is unchanged. See #65817. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy --- src/wp-includes/media.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 880848c71b103..b2bb821670a27 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -4223,6 +4223,7 @@ function get_attachment_taxonomies( $attachment, $output = 'names' ) { * @param string $output Optional. The type of taxonomy output to return. Accepts 'names' or 'objects'. * Default 'names'. * @return string[]|WP_Taxonomy[] Array of names or objects of registered taxonomies for attachments. + * @phpstan-return ( $output is 'names' ? list : array ) */ function get_taxonomies_for_attachments( $output = 'names' ) { $taxonomies = array(); From d648f5bb16e2b93052724dd77cf55e4e31be5ebc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 31 Aug 2026 09:24:57 +0000 Subject: [PATCH 6/6] Apply suggestions from code review. - Narrow key and value types to `non-falsy-string` in the conditional return types for `get_post_stati()`, `get_taxonomies()`, `get_object_taxonomies()`, `get_attachment_taxonomies()`, and `get_taxonomies_for_attachments()`. - Narrow the `$wp_taxonomies` global type for `get_object_taxonomies()`. - Ensure `get_attachment_taxonomies()` returns a list when plucking names. Co-authored-by: Weston Ruter Co-authored-by: Claude --- src/wp-includes/media.php | 8 +++++--- src/wp-includes/post.php | 2 +- src/wp-includes/taxonomy.php | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index b2bb821670a27..580ba2cb751ce 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -4160,7 +4160,7 @@ function adjacent_image_link( $prev = true, $size = 'thumbnail', $text = false ) * or 'objects' to return an array of taxonomy objects. * Default is 'names'. * @return string[]|WP_Taxonomy[] List of taxonomies or taxonomy names. Empty array on failure. - * @phpstan-return ( $output is 'names' ? array : WP_Taxonomy[] ) + * @phpstan-return ( $output is 'names' ? list : array ) */ function get_attachment_taxonomies( $attachment, $output = 'names' ) { if ( is_int( $attachment ) ) { @@ -4205,7 +4205,9 @@ function get_attachment_taxonomies( $attachment, $output = 'names' ) { } if ( 'names' === $output ) { - $taxonomies = array_unique( $taxonomies ); + /** @var non-falsy-string[] $taxonomies */ + $unique_taxonomies = array_values( array_unique( $taxonomies ) ); + $taxonomies = $unique_taxonomies; } return $taxonomies; @@ -4223,7 +4225,7 @@ function get_attachment_taxonomies( $attachment, $output = 'names' ) { * @param string $output Optional. The type of taxonomy output to return. Accepts 'names' or 'objects'. * Default 'names'. * @return string[]|WP_Taxonomy[] Array of names or objects of registered taxonomies for attachments. - * @phpstan-return ( $output is 'names' ? list : array ) + * @phpstan-return ( $output is 'names' ? list : array ) */ function get_taxonomies_for_attachments( $output = 'names' ) { $taxonomies = array(); diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 3cde0bfe58936..2ac3b18212c0d 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -1575,7 +1575,7 @@ function get_post_status_object( $post_status ) { * from the array needs to match; 'and' means all elements must match. * Default 'and'. * @return string[]|stdClass[] A list of post status names or objects. - * @phpstan-return ( $output is 'names' ? string[] : stdClass[] ) + * @phpstan-return ( $output is 'names' ? array : array ) */ function get_post_stati( $args = array(), $output = 'names', $operator = 'and' ) { global $wp_post_statuses; diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index de6ee7598c74f..533a725b4707b 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -278,7 +278,7 @@ function create_initial_taxonomies() { * one element from the array needs to match; 'and' means all elements must match. * Default 'and'. * @return string[]|WP_Taxonomy[] An array of taxonomy names or objects. - * @phpstan-return ( $output is 'names' ? string[] : WP_Taxonomy[] ) + * @phpstan-return ( $output is 'names' ? array : array ) */ function get_taxonomies( $args = array(), $output = 'names', $operator = 'and' ) { global $wp_taxonomies; @@ -302,13 +302,13 @@ function get_taxonomies( $args = array(), $output = 'names', $operator = 'and' ) * * @since 2.3.0 * - * @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies. + * @global array $wp_taxonomies The registered taxonomies. * * @param string|string[]|WP_Post $object_type Name of the type of taxonomy object, or an object (row from posts). * @param string $output Optional. The type of output to return in the array. Accepts either * 'names' or 'objects'. Default 'names'. * @return string[]|WP_Taxonomy[] The names or objects of all taxonomies of `$object_type`. - * @phpstan-return ( $output is 'names' ? array : WP_Taxonomy[] ) + * @phpstan-return ( $output is 'names' ? list : array ) */ function get_object_taxonomies( $object_type, $output = 'names' ) { global $wp_taxonomies;