Improve typing and documentation for wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list()#12588
Improve typing and documentation for wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list()#12588westonruter wants to merge 23 commits into
wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list()#12588Conversation
|
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. |
wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list()
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. |
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>
wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list()wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list()
There was a problem hiding this comment.
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(), andwp_parse_slug_list(). - Hardens
wp_parse_list()againstpreg_split()returningfalseby returning an empty array on failure. - Updates
wp_parse_slug_list()to cast values to strings beforesanitize_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.
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
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
✅ Committed in r62797 (983a066).
The
wp_parse_list(),wp_parse_id_list(), andwp_parse_slug_list()functions are key utility functions. Thewp_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: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|stringbecomes@param mixed[]|stringon 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 meansarray<string>.The conditional does not extend to
wp_parse_id_list()orwp_parse_slug_list(), becausearray_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 declaredarrayreturn didn't account forpreg_split()returningfalseon a PCRE runtime failure. Casting the result would turn that intoarray( false )rather than an empty array, and the strayfalsewould go on to reachabsint()as0orsanitize_title()as'', adding a phantom entry. The result is checked withis_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 throughstrval()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 thecallable(mixed): mixedmismatch go away honestly.Docs. The
@returndescriptions now say why the result isn't necessarily a list — keys carried over from an array argument, and gaps left byarray_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 withassertSame()so key order and types are actually checked. Each test also asserts the documented value contract (scalar/ non-negativeint/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
listinstead ofarray, bypassing through the value througharray_values(). The use ofarray_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:So
array( 'date' => 'DATE' )means "group bydate, cast it as DATE".array_values()silently discards the key, so$fieldbecomes the cast type'DATE'— the generatedGROUP BYandSELECT ... ASclauses 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
foreachover the result: exactly those 2 plugins, ecosystem-wide (0 in themes).array_keys( wp_parse_*list(...) )— 0 hits.wp_parse_id_list( wp_list_pluck( ... ) )call sites use the two-argwp_list_pluck(noindex_key), so they pass lists. No assoc array literals passed in anywhere.implode,in_array,array_merge,foreachby value. Andclass-wp-widget-media-gallery.php:72useswp_parse_id_listas asanitize_callback; today a duplicate ID leaves gapped keys, sowp_json_encodeemits{"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()— keeparray_values(). Zero evidence of key dependence, and it fixes the gapped-key JSON issue.list<...>is honest.wp_parse_list()— I'd drop thearray_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 justwp_parse_list()— the gapped-key JSON behavior it would have fixed inwp_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 againsttrunk:trunkThe 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 asarray<mixed>where barearrayrendered asarray, so every unchanged call site's message text shifted. Separately, nine pre-existing errors at unchanged call sites are now reported with narrower types (mixed→bool|float|int|string), which makes them actionable:trim()receivingmixedis unfixable noise, whereastrim()receivingbool|float|int|stringis 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@returndocumentation).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.