Code Quality: Add conditional return types for field-plucking functions - #13307
Code Quality: Add conditional return types for field-plucking functions#13307swissspidy wants to merge 5 commits into
Conversation
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
|
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 Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe 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
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
| * 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[] ) |
There was a problem hiding this comment.
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.
| * 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[] ) |
There was a problem hiding this comment.
The keys are the statuses as well, so this can be narrowed:
| * @phpstan-return ( $output is 'names' ? string[] : stdClass[] ) | |
| * @phpstan-return ( $output is 'names' ? array<non-falsy-string, non-falsy-string> : array<non-falsy-string, stdClass> ) |
| * @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> ) |
There was a problem hiding this comment.
Perhaps overly narrow, but why not:
| * @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. |
There was a problem hiding this comment.
| * @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>.
There was a problem hiding this comment.
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[] ) |
There was a problem hiding this comment.
They keys are the taxonomy names:
| * @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[] ) |
There was a problem hiding this comment.
| * @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> ) |
| * 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[] ) |
There was a problem hiding this comment.
| * @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> ) |
| if ( 'names' === $output ) { | ||
| $taxonomies = array_unique( $taxonomies ); | ||
| } |
There was a problem hiding this comment.
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:
| if ( 'names' === $output ) { | |
| /** @var non-falsy-string[] $taxonomies */ | |
| $unique_taxonomies = array_values( array_unique( $taxonomies ) ); | |
| $taxonomies = $unique_taxonomies; | |
| } |
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.