Skip to content

Code Quality: Add conditional return types for field-plucking functions - #13307

Open
swissspidy wants to merge 5 commits into
WordPress:trunkfrom
swissspidy:claude/get-post-stati-return-type
Open

Code Quality: Add conditional return types for field-plucking functions#13307
swissspidy wants to merge 5 commits into
WordPress:trunkfrom
swissspidy:claude/get-post-stati-return-type

Conversation

@swissspidy

@swissspidy swissspidy commented Aug 28, 2026

Copy link
Copy Markdown
Member

Trac ticket: https://core.trac.wordpress.org/ticket/65817

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Used for: Whole implementation


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

swissspidy and others added 5 commits August 28, 2026 11:30
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy
`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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy
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<int, string>` says so where
`string[]` leaves the key as `array-key`.

It is deliberately not `list<string>`, 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<int<0, max>, 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy
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<int, string>`. `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<int, string>` rather than claiming `list<string>` -- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy
…s()`.

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<string, WP_Taxonomy>` 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<string>`, not `array<int, string>`. 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CG1pXsNoUnbjzti49c1Zy
Copilot AI lite review requested due to automatic review settings August 28, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props swissspidy.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Comment thread src/wp-includes/post.php
* 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[] )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside: I'd like to make stdClass more specific, and use it in get_post_status_object() and other places as well. We can define a new global type alias as proposed by @johnbillion in westonruter#6

Pending commit in #12443

This can be done in another PR, of course.

Comment thread src/wp-includes/post.php
* 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[] )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keys are the statuses as well, so this can be narrowed:

Suggested change
* @phpstan-return ( $output is 'names' ? string[] : stdClass[] )
* @phpstan-return ( $output is 'names' ? array<non-falsy-string, non-falsy-string> : array<non-falsy-string, stdClass> )

Comment thread src/wp-includes/media.php
* @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<string> : array<string, WP_Taxonomy> )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps overly narrow, but why not:

Suggested change
* @phpstan-return ( $output is 'names' ? list<string> : array<string, WP_Taxonomy> )
* @phpstan-return ( $output is 'names' ? list<non-falsy-string> : array<non-falsy-string, WP_Taxonomy> )

*
* @since 2.3.0
*
* @global WP_Taxonomy[] $wp_taxonomies The registered taxonomies.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @global array<non-falsy-string, WP_Taxonomy> $wp_taxonomies The registered taxonomies.

This fixes a PHPStan error at the return:

phpstan: Function get_object_taxonomies() should return array<string|WP_Taxonomy> but returns array<int|string|WP_Taxonomy>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, this would make sense to do holistically across all @global tags.

* 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[] )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They keys are the taxonomy names:

Suggested change
* @phpstan-return ( $output is 'names' ? string[] : WP_Taxonomy[] )
* @phpstan-return ( $output is 'names' ? array<non-falsy-string, non-falsy-string> : array<non-falsy-string, WP_Taxonomy> )

* @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<int, string> : WP_Taxonomy[] )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @phpstan-return ( $output is 'names' ? array<int, string> : WP_Taxonomy[] )
* @phpstan-return ( $output is 'names' ? list<non-falsy-string> : array<non-falsy-string, WP_Taxonomy> )

Comment thread src/wp-includes/media.php
* 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<int, string> : WP_Taxonomy[] )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @phpstan-return ( $output is 'names' ? array<int, string> : WP_Taxonomy[] )
* @phpstan-return ( $output is 'names' ? list<non-falsy-string> : array<non-falsy-string, WP_Taxonomy> )

Comment thread src/wp-includes/media.php
Comment on lines 4207 to 4209
if ( 'names' === $output ) {
$taxonomies = array_unique( $taxonomies );
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHPStan here is complaining:

phpstan: Parameter #1 $array of function array_unique expects an array of values castable to string, array<int<0, max>|string, string|WP_Taxonomy> given.

It isn't narrowing $taxonomies as it should be, so it is flatting the use of the default SORT_STRING.

Also, the use of array_unique() is resulting in a possible non-list return value. Adding array_values() would fix this.

The following allows for the above narrowed return type:

Suggested change
if ( 'names' === $output ) {
/** @var non-falsy-string[] $taxonomies */
$unique_taxonomies = array_values( array_unique( $taxonomies ) );
$taxonomies = $unique_taxonomies;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants