Skip to content

Improve typing and documentation for wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list()#12588

Closed
westonruter wants to merge 23 commits into
WordPress:trunkfrom
westonruter:update/parse-list-typing
Closed

Improve typing and documentation for wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list()#12588
westonruter wants to merge 23 commits into
WordPress:trunkfrom
westonruter:update/parse-list-typing

Conversation

@westonruter

@westonruter westonruter commented Jul 19, 2026

Copy link
Copy Markdown
Member

✅ Committed in r62797 (983a066).


The wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list() functions are key utility functions. The wp_parse_id_list() function was used in a security fix for 7.0.2 in r62771 (97b5a75). However, these functions can be further hardened to give precise types and to address various PHPStan errors at rule level 10:

wp_parse_list() (5027–5029)

  • missingType.iterableValue ×2 — $input_list param and return are bare array.
  • return.typepreg_split() can return false, which isn't in the declared array return.

wp_parse_id_list() (5047)

  • missingType.iterableValue — bare array param.

wp_parse_slug_list() (5062, 5065)

  • missingType.iterableValue — bare array param.
  • argument.typearray_map( 'sanitize_title', ... ): sanitize_title()'s first param is string, but the array's value type is mixed, so it isn't accepted as callable(mixed): mixed. (absint in wp_parse_id_list doesn't trip this because its param is mixed.)

What this changes

No return value changes for any input, with one exception noted below. The keys, values, and ordering that callers see today are all preserved.

  • Types. @param array|string becomes @param mixed[]|string on all three. wp_parse_list() gains @phpstan-return ($input_list is string ? list<string> : array<scalar>) — splitting a string yields sequential keys, so that branch really is a list, while the array branch carries its argument's keys through. wp_parse_id_list() gains @phpstan-return array<non-negative-int>; wp_parse_slug_list() needs no PHPStan tag, since @return string[] already means array<string>.

    The conditional does not extend to wp_parse_id_list() or wp_parse_slug_list(), because array_unique() keeps the first occurrence of a duplicate and so leaves gaps in the keys even when a string was passed: wp_parse_id_list( '1,2,2,3' ) returns [0 => 1, 1 => 2, 3 => 3].

  • preg_split() failure handling. The declared array return didn't account for preg_split() returning false on a PCRE runtime failure. Casting the result would turn that into array( false ) rather than an empty array, and the stray false would go on to reach absint() as 0 or sanitize_title() as '', adding a phantom entry. The result is checked with is_array() instead. This is the one behavior change, and only on a code path that previously produced a wrong value.

  • sanitize_title() input. Values are passed through strval() first. wp_parse_list() has already filtered out non-scalars, so this only affects booleans, integers, and floats — which PHP was coercing identically anyway. No output changes; it just makes the callable(mixed): mixed mismatch go away honestly.

  • Docs. The @return descriptions now say why the result isn't necessarily a list — keys carried over from an array argument, and gaps left by array_unique() — rather than leaving it to be inferred from the implementation.

  • Tests. Coverage for associative arrays, non-scalar entries, and duplicate values, with the resulting keys asserted explicitly. assertSameSets() is replaced with assertSame() so key order and types are actually checked. Each test also asserts the documented value contract (scalar / non-negative int / string) independently of what the data provider expects, so a regression in the function is caught even if a provider row is updated to match the new output.

Why not lists

Originally, I intended to type these so that they returned list instead of array, bypassing through the value through array_values(). The use of array_filter() can cause sparse arrays to be returned unexpectedly, and also for passed associative to have their keys passed through. However, passing in an associative array and getting those same string keys back out appears to be done intentionally in at least one case.

Claude Research of Veloria

Checked Veloria (latest versions of all wp.org-hosted plugins + themes). Here's what I found.

Yes — there's one real break, and it's in wp_parse_list()

The Hizzle store library deliberately passes an associative array and branches on the string key:

vendor/hizzle/store/src/Query.php:472 — shipped in Noptin (10k installs) and Hizzle Downloads:

foreach ( wp_parse_list( $qv['groupby'] ) as $index => $field ) {
    $cast = false;
    // Check if $index is a string and different from $field
    if ( is_string( $index ) && $index !== $field ) {
        $cast  = $field;
        $field = $index;
    }
    ...
    $this->query_groupby .= ', `' . $field . '`';

So array( 'date' => 'DATE' ) means "group by date, cast it as DATE". array_values() silently discards the key, so $field becomes the cast type 'DATE' — the generated GROUP BY and SELECT ... AS clauses would reference a nonexistent column. That's a fatal SQL error or wrong aggregation in their reporting queries, failing silently at the WP level.

Everything else is clean

  • Key-consuming foreach over the result: exactly those 2 plugins, ecosystem-wide (0 in themes). array_keys( wp_parse_*list(...) ) — 0 hits.
  • Assoc arrays going in: all 29 wp_parse_id_list( wp_list_pluck( ... ) ) call sites use the two-arg wp_list_pluck (no index_key), so they pass lists. No assoc array literals passed in anywhere.
  • Core: every usage is key-agnostic — implode, in_array, array_merge, foreach by value. And class-wp-widget-media-gallery.php:72 uses wp_parse_id_list as a sanitize_callback; today a duplicate ID leaves gapped keys, so wp_json_encode emits {"0":1,"2":3} instead of an array. Your change actually fixes that latent wart.

Recommendation

The risk is asymmetric across the three functions, so I'd split them:

  • wp_parse_id_list() / wp_parse_slug_list() — keep array_values(). Zero evidence of key dependence, and it fixes the gapped-key JSON issue. list<...> is honest.
  • wp_parse_list() — I'd drop the array_values(). It's the general-purpose one, its keys carry meaning for at least one real consumer, and unlike the other two it does no per-element transformation, so preserving the input's shape is arguably its contract. Declare @phpstan-return ($input_list is string ? list<string> : array<array-key, scalar>) instead.

One caveat on the evidence: Veloria indexes the current release of wp.org-hosted plugins and themes only — premium plugins, agency/client code, and mu-plugins aren't covered. Finding a deliberate key-dependent consumer in the first sweep of a fairly obscure pattern suggests the unmeasured tail probably holds a few more.

Therefore, it is safer to not change these functions for risk of a back-compat breakage. To obtain true lists, the return value can be passed through array_values().

Note: the per-function split recommended at the end of that research was not taken. array_values() is dropped from all three functions, not just wp_parse_list() — the gapped-key JSON behavior it would have fixed in wp_parse_id_list() isn't worth a separate compatibility risk in a PR that is otherwise type-only.

PHPStan impact

Measured with two cold-cache runs over src/, comparing this branch against trunk:

Metric Value
Errors in trunk 32,863
Errors in this branch 32,857
Net change −6

The six are exactly the errors quoted at the top, all in wp-includes/functions.php. Nothing is introduced.

The raw diff also reports ~52 fixed/introduced pairs that are purely cosmetic — @param mixed[] renders as array<mixed> where bare array rendered as array, so every unchanged call site's message text shifted. Separately, nine pre-existing errors at unchanged call sites are now reported with narrower types (mixedbool|float|int|string), which makes them actionable: trim() receiving mixed is unfixable noise, whereas trim() receiving bool|float|int|string is a concrete finding.

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

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Opus 4.8
Used for: Coming up with some test assertion logic. Obtaining PHPStan errors and measuring the branch-vs-trunk impact. Surveying the plugin/theme ecosystem for back-compat risk. Reviewing the change, and authoring the follow-up commits that came out of that review (the preg_split() failure handling, the conditional return type, the return-contract test assertions, and the @return documentation).


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.

@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 westonruter.

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

@westonruter westonruter changed the title Update/parse list typing Improve typing for wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list() Jul 19, 2026
@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.

@westonruter
westonruter marked this pull request as draft July 19, 2026 02:20
@westonruter
westonruter marked this pull request as ready for review July 19, 2026 02:37
westonruter and others added 6 commits July 18, 2026 20:15
A PCRE runtime failure makes preg_split() return false, and casting that
yields array( false ) rather than an empty array. The stray false then
reaches absint() as 0 and sanitize_title() as an empty string, adding a
phantom entry to the parsed list. Check for an array instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The callback checks that every entry is a string, but the failure message
was carried over from the wp_parse_id_list() test and claimed the array
should contain only non-negative ints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wp_parse_id_list() and wp_parse_slug_list() tests already assert that
every returned entry matches the documented type, but wp_parse_list() only
compared against the expected value. Add the matching scalar check so a
regression in the function is caught even if a data provider is updated to
match the new output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Saying only that the return value is not guaranteed to be a list left the
reason to be inferred from the implementation. Name the two causes instead:
keys are carried over from an array argument, and array_unique() keeps the
first occurrence of a duplicate, leaving gaps behind.

Key preservation is load-bearing for callers that pass an associative array
to wp_parse_list() and read the keys back, so it is worth stating outright.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Splitting a string yields sequential keys, so the result is a list; only the
array branch carries keys over from its argument. A conditional return type
lets callers that pass a known string benefit from that.

This is not applicable to wp_parse_id_list() and wp_parse_slug_list(), where
array_unique() keeps the first occurrence of a duplicate and so leaves gaps
in the keys even when a string was passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The conditional return type spells this out for static analysis, but the
description only mentioned the array case, leaving a reader to infer the
rest. Cover both branches so the two tags say the same thing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@westonruter westonruter changed the title Improve typing for wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list() Improve typing and documentation for wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list() Jul 19, 2026
@westonruter
westonruter requested a review from Copilot July 19, 2026 05:14

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.

Pull request overview

This PR improves typing and documentation for the core utility functions wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list() in wp-includes/functions.php, and strengthens PHPUnit coverage around key preservation, filtering behavior, and duplicate-handling to better align with the functions’ real-world contracts and PHPStan expectations.

Changes:

  • Refines docblocks (including PHPStan-specific return typing) for wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list().
  • Hardens wp_parse_list() against preg_split() returning false by returning an empty array on failure.
  • Updates wp_parse_slug_list() to cast values to strings before sanitize_title(), and expands PHPUnit assertions to validate element types and key behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/wp-includes/functions.php Updates typing/docs, adds preg_split() failure handling, and adjusts slug parsing to ensure string inputs to sanitize_title().
tests/phpunit/tests/functions/wpParseList.php Adds stronger assertions for scalar-only output and key preservation cases.
tests/phpunit/tests/functions/wpParseIdList.php Adds stronger assertions for non-negative-int output and key behavior (including gaps).
tests/phpunit/tests/functions/wpParseSlugList.php Adds stronger assertions for string-only output, key preservation, and unexpected-input cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/wp-includes/functions.php
pento pushed a commit that referenced this pull request Jul 19, 2026
This adds precise `@param` and `@return` typing for the `wp_parse_list()`, `wp_parse_id_list()`, and `wp_parse_slug_list()` functions; native `array` return types are also added. It also adds casting and type guarding to guarantee the types of the values involved. Descriptions are updated to indicate that lists may not be returned, which may be unexpected given the function names; instead, sparse arrays or even associative arrays may be returned. Additionally, typically invalid ID values like zero may be in the array returned by `wp_parse_id_list()` and an empty string may be in the array returned by `wp_parse_slug_list()`.

Tests are added to ensure existing behavior is preserved. This fixes 6 PHPStan errors at rule level 10.

Developed in #12588.
Follow-up to r38832, r44546, r57737, r62647, r62771.

See #64898.


git-svn-id: https://develop.svn.wordpress.org/trunk@62797 602fd350-edb4-49c9-b593-d223f7449a82
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Jul 19, 2026
This adds precise `@param` and `@return` typing for the `wp_parse_list()`, `wp_parse_id_list()`, and `wp_parse_slug_list()` functions; native `array` return types are also added. It also adds casting and type guarding to guarantee the types of the values involved. Descriptions are updated to indicate that lists may not be returned, which may be unexpected given the function names; instead, sparse arrays or even associative arrays may be returned. Additionally, typically invalid ID values like zero may be in the array returned by `wp_parse_id_list()` and an empty string may be in the array returned by `wp_parse_slug_list()`.

Tests are added to ensure existing behavior is preserved. This fixes 6 PHPStan errors at rule level 10.

Developed in WordPress/wordpress-develop#12588.
Follow-up to r38832, r44546, r57737, r62647, r62771.

See #64898.

Built from https://develop.svn.wordpress.org/trunk@62797


git-svn-id: http://core.svn.wordpress.org/trunk@62077 1a063a9b-81f0-0310-95a4-ce76da25c4cd
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.

2 participants