Skip to content

Code Quality: Complete the conditional return types for post and term queries - #13216

Open
swissspidy wants to merge 2 commits into
WordPress:trunkfrom
swissspidy:claude/phpstan-wordpress-return-types-if6ibn
Open

Code Quality: Complete the conditional return types for post and term queries#13216
swissspidy wants to merge 2 commits into
WordPress:trunkfrom
swissspidy:claude/phpstan-wordpress-return-types-if6ibn

Conversation

@swissspidy

@swissspidy swissspidy commented Aug 21, 2026

Copy link
Copy Markdown
Member

Follow-up to r62648, which added conditional return types keyed on the fields argument to get_posts() and WP_Query::query().

'id=>parent' was missing from both conditions. Both only recognise 'ids', but 'id=>parent' also returns an array of integers — parent post IDs keyed by post ID, per the switch in WP_Query::get_posts() and the $post_parents return a few hundred lines below it. Today get_posts( [ 'fields' => 'id=>parent' ] ) is reported as WP_Post[], which is wrong in a way that is worse than no annotation at all.

WP_Term_Query::query() can be annotated the same way. Like WP_Query::query(), it takes the query arguments directly, so the mapping already documented in prose on WP_Term_Query::get_terms() can be expressed as a type:

  • 'count' returns 0|numeric-string.
  • 'ids', 'tt_ids' and 'id=>parent' return arrays of integers.
  • 'names', 'slugs', 'id=>name' and 'id=>slug' return arrays of strings.
  • Everything else returns an array of WP_Term objects.

The integer in the count branch is not a typo. WP_Term_Query::get_terms() bails out early with an integer rather than the numeric string the docblock describes when child_of or parent names a term with no descendants:

if ( ! $in_hierarchy ) {
    if ( 'count' === $args['fields'] ) {
        return 0;
    }

so get_terms( [ 'fields' => 'count', 'parent' => $id ] ) really can return 0. Every other count path returns $wpdb->get_var()'s numeric string. Annotating numeric-string alone would have been a promise core does not keep. (The @return line on both methods says only string for this case, and the $fields entry in WP_Term_Query::__construct() says int — that inconsistency predates this patch and is left alone here.)

WP_Query::get_posts() and WP_Term_Query::get_terms() read the query vars off the instance rather than taking them as an argument, so there is nothing to narrow their return types on and they are unchanged.

Analysis impact

Verified with PHPStan 2.2 against extracted copies of the three annotated docblocks:

  • All three docblocks parse; no phpDoc.parseError.
  • Each branch resolves as intended, and unsealed shapes ({ fields: 'ids', ... }) still match when other query vars are present.
  • get_terms() in taxonomy.php — the only core caller of WP_Term_Query::query() — produces no new errors before or after, including the widened count branch. PHPStan already normalises the existing @return WP_Term[]|int[]|string[]|string into a single array type unioned with string, so nothing widens. (Confirmed the return.type rule was actually live in that harness by deliberately breaking it.)
  • Neither core call site of 'id=>parent' (wp_edit_posts_query() via wp(), and _get_term_hierarchy() via get_terms()) goes through the changed conditions, so no baseline entries are affected.

The full core PHPStan run has not been executed against this branch.

Why this matters downstream

php-stubs/wordpress-stubs carries its own functionMap.php entry for get_posts() that covers both 'ids' and 'id=>parent'. Once the stubs bump past 7.0.1, that entry becomes a duplicate of the core annotation and will be removed as obsolete — exactly what php-stubs/wordpress-stubs#475 did for WP_Theme::get(). Without this patch, that removal would silently regress 'id=>parent' to WP_Post[]. The matching stubs change is at swissspidy/wordpress-stubs#1.

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

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Used for: Locating the gap and writing the three docblock annotations


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.

… queries.

`get_posts()` and `WP_Query::query()` narrow their return type on the
`fields` argument, but only recognise `'ids'`. `'id=>parent'` also
returns an array of integers (parent post IDs keyed by post ID), and was
being reported as `WP_Post[]`. Add it to both conditions.

`WP_Term_Query::query()` takes the query arguments directly, so its
return type can be resolved the same way. Annotate it with the mapping
already documented on `WP_Term_Query::get_terms()`:

* `'count'` returns `0|numeric-string`. The integer covers the early
  `return 0;` in `WP_Term_Query::get_terms()`, taken when `'child_of'`
  or `'parent'` names a term with no descendants — every other count
  path returns the numeric string the docblock describes.
* `'ids'`, `'tt_ids'` and `'id=>parent'` return arrays of integers.
* `'names'`, `'slugs'`, `'id=>name'` and `'id=>slug'` return arrays of
  strings.
* Everything else returns an array of `WP_Term` objects.

The sibling `WP_Query::get_posts()` and `WP_Term_Query::get_terms()`
methods read the query vars off the instance rather than taking them as
an argument, so their return types cannot be narrowed this way.

Follow-up to r62648.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZsZWUzYLafHeu6RXnLMUy
@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.

@swissspidy
swissspidy marked this pull request as ready for review August 21, 2026 10:02
@swissspidy
swissspidy requested review from westonruter and a lite review from Copilot August 21, 2026 10:02

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

github-actions Bot commented Aug 21, 2026

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.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @claude.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

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

Props westonruter.

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

*
* @phpstan-return (
* $query is array{ fields: 'ids', ... } ? int[] : WP_Post[]
* $query is array{ fields: 'ids'|'id=>parent', ... } ? int[] : WP_Post[]

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.

Shouldn't there be an additional case for'id=>parent' which returns an explicit array<int, int>? Granted, int[] also describes this, but in a less specific way: the keys could be strings too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually, it seems like when an object cache is in use, it returns the raw wp_cache_get_multiple() result, which is keyed by the
cache keys, i.e. "post_parent:{$id}" strings. So int[] would be nore correct. Or array<non-falsy-string, int>|array<int, int>

Comment thread src/wp-includes/class-wp-term-query.php Outdated
Comment thread src/wp-includes/post.php
*
* @phpstan-return (
* $args is array{ fields: 'ids', ... } ? int[] : WP_Post[]
* $args is array{ fields: 'ids'|'id=>parent', ... } ? int[] : WP_Post[]

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.

Ditto above

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same here about the object cache case

… …>`.

Address review feedback on the conditional return type added to
`WP_Term_Query::query()`. `int[]` and `string[]` are shorthand for
`array<array-key, int>` and `array<array-key, string>`, which say nothing
about the keys. For the three `'id=>…'` fields the keys are the point:
`WP_Term_Query::format_terms()` builds them from `WP_Term::$term_id`,
which `sanitize_term_field()` casts to `int`. Give those fields their own
branches returning `array<int, int>` and `array<int, string>`, and leave
`'ids'`, `'tt_ids'`, `'names'` and `'slugs'` as `int[]`/`string[]`, whose
keys are positional and carry no meaning.

While here, correct the field list on `WP_Term_Query::get_terms()`, which
grouped `'id=>parent'` under "an array of numeric strings". It returns
parent term IDs as integers, as the `$fields` argument is already
documented to do in `WP_Term_Query::__construct()`.

Follow-up to r62648.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUwqNiMhXKBTa8GJyKzEZH
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.

4 participants