From 99c6fcbee0fdbe0d87b7535ae3489bf9d7f2c10c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 09:58:44 -0700 Subject: [PATCH 01/22] Ensure wp_parse_list() returns list --- src/wp-includes/functions.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 6a7333d317b71..d7d72657ddc4e 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5023,6 +5023,7 @@ function wp_parse_args( $args, $defaults = array() ) { * * @param array|string $input_list List of values. * @return array Array of values. + * @phpstan-return list */ function wp_parse_list( $input_list ) { if ( ! is_array( $input_list ) ) { @@ -5030,7 +5031,7 @@ function wp_parse_list( $input_list ) { } // Validate all entries of the list are scalar. - $input_list = array_filter( $input_list, 'is_scalar' ); + $input_list = array_values( array_filter( $input_list, 'is_scalar' ) ); return $input_list; } From 3c8203c2d0682f65ee559e6ba16fd20363d484b4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 09:59:04 -0700 Subject: [PATCH 02/22] Type wp_parse_id_list() return as list --- src/wp-includes/functions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index d7d72657ddc4e..f09172a01b1ea 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5044,6 +5044,7 @@ function wp_parse_list( $input_list ) { * * @param array|string $input_list List of IDs. * @return int[] Sanitized array of IDs. + * @phpstan-return list */ function wp_parse_id_list( $input_list ) { $input_list = wp_parse_list( $input_list ); From 5175858327672e440112098382cd47eb7d93168b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:08:48 -0700 Subject: [PATCH 03/22] Fix remaining PHPStan issues with wp_parse_list() --- src/wp-includes/functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index f09172a01b1ea..9ae93dd73ad56 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5021,13 +5021,13 @@ function wp_parse_args( $args, $defaults = array() ) { * * @since 5.1.0 * - * @param array|string $input_list List of values. + * @param mixed[]|string $input_list List of values. * @return array Array of values. * @phpstan-return list */ function wp_parse_list( $input_list ) { if ( ! is_array( $input_list ) ) { - return preg_split( '/[\s,]+/', $input_list, -1, PREG_SPLIT_NO_EMPTY ); + return (array) preg_split( '/[\s,]+/', $input_list, -1, PREG_SPLIT_NO_EMPTY ); } // Validate all entries of the list are scalar. From 975f570ea1a22677471f707f5db31081995bd4b9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:09:45 -0700 Subject: [PATCH 04/22] Ensure wp_parse_id_list() returns a list --- src/wp-includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 9ae93dd73ad56..232564b9f9dc5 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5049,7 +5049,7 @@ function wp_parse_list( $input_list ) { function wp_parse_id_list( $input_list ) { $input_list = wp_parse_list( $input_list ); - return array_unique( array_map( 'absint', $input_list ) ); + return array_values( array_unique( array_map( 'absint', $input_list ) ) ); } /** From 756bf6a0893d366f0302a76bbe6c699a447da871 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:10:53 -0700 Subject: [PATCH 05/22] Fix phpstan complaint about under-typed array param --- src/wp-includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 232564b9f9dc5..4758420e2f16c 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5042,7 +5042,7 @@ function wp_parse_list( $input_list ) { * @since 3.0.0 * @since 5.1.0 Refactored to use wp_parse_list(). * - * @param array|string $input_list List of IDs. + * @param mixed[]|string $input_list List of IDs. * @return int[] Sanitized array of IDs. * @phpstan-return list */ From d86624d197e7074569ebb5373f39f82b56a66434 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:25:43 -0700 Subject: [PATCH 06/22] Explicitly cast values to strings before passing into sanitize_title() --- src/wp-includes/functions.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 4758420e2f16c..5ede749b015e8 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5058,13 +5058,28 @@ function wp_parse_id_list( $input_list ) { * @since 4.7.0 * @since 5.1.0 Refactored to use wp_parse_list(). * - * @param array|string $input_list List of slugs. + * @param mixed[]|string $input_list List of slugs. * @return string[] Sanitized array of slugs. + * @phpstan-return list */ function wp_parse_slug_list( $input_list ) { $input_list = wp_parse_list( $input_list ); - return array_unique( array_map( 'sanitize_title', $input_list ) ); + return array_values( + array_unique( + array_map( + 'sanitize_title', + array_map( + /* + * Cast booleans, integers, and floats to strings. Non-scalar types (including null) have already + * been filtered out by wp_parse_list(). + */ + 'strval', + $input_list + ) + ) + ) + ); } /** From 91074b995726a3bc9b78d22f1722b6c432e8941a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:27:00 -0700 Subject: [PATCH 07/22] Indicate that returned values may include zero and empty string --- src/wp-includes/functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 5ede749b015e8..cc40408f53d94 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5043,7 +5043,7 @@ function wp_parse_list( $input_list ) { * @since 5.1.0 Refactored to use wp_parse_list(). * * @param mixed[]|string $input_list List of IDs. - * @return int[] Sanitized array of IDs. + * @return int[] Sanitized array of IDs. May include zero. * @phpstan-return list */ function wp_parse_id_list( $input_list ) { @@ -5059,7 +5059,7 @@ function wp_parse_id_list( $input_list ) { * @since 5.1.0 Refactored to use wp_parse_list(). * * @param mixed[]|string $input_list List of slugs. - * @return string[] Sanitized array of slugs. + * @return string[] Sanitized array of slugs. May include an empty string. * @phpstan-return list */ function wp_parse_slug_list( $input_list ) { From e6ae6063e7d6334caf84ea34bde7b3fa1a351367 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:37:03 -0700 Subject: [PATCH 08/22] Add wp_parse_list() tests for non-scalars and list return value --- tests/phpunit/tests/functions/wpParseList.php | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/tests/phpunit/tests/functions/wpParseList.php b/tests/phpunit/tests/functions/wpParseList.php index 2f7bf086e4193..fef1787becf7c 100644 --- a/tests/phpunit/tests/functions/wpParseList.php +++ b/tests/phpunit/tests/functions/wpParseList.php @@ -13,62 +13,75 @@ class Tests_Functions_wpParseList extends WP_UnitTestCase { * @ticket 43977 * * @dataProvider data_wp_parse_list + * + * @param mixed[]|string $input_list + * @param list $expected */ - public function test_wp_parse_list( $input_list, $expected ) { - $this->assertSameSets( $expected, wp_parse_list( $input_list ) ); + public function test_wp_parse_list( $input_list, array $expected ): void { + $parsed_list = wp_parse_list( $input_list ); + $this->assertTrue( array_is_list( $parsed_list ), 'Expected value to be a list.' ); + $this->assertSameSets( $expected, $parsed_list ); } /** * Data provider. * - * @return array[] + * @return array }> */ - public function data_wp_parse_list() { + public function data_wp_parse_list(): array { return array( - 'ids only' => array( + 'ids only' => array( 'input_list' => '1,2,3,4', 'expected' => array( '1', '2', '3', '4' ), ), - 'slugs only' => array( + 'slugs only' => array( 'input_list' => 'apple,banana,carrot,dog', 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), ), - 'ids and slugs' => array( + 'ids and slugs' => array( 'input_list' => '1,2,apple,banana', 'expected' => array( '1', '2', 'apple', 'banana' ), ), - 'space after comma' => array( + 'space after comma' => array( 'input_list' => '1, 2,apple,banana', 'expected' => array( '1', '2', 'apple', 'banana' ), ), - 'double comma' => array( + 'double comma' => array( 'input_list' => '1,2,apple,,banana', 'expected' => array( '1', '2', 'apple', 'banana' ), ), - 'leading comma' => array( + 'leading comma' => array( 'input_list' => ',1,2,apple,banana', 'expected' => array( '1', '2', 'apple', 'banana' ), ), - 'trailing comma' => array( + 'trailing comma' => array( 'input_list' => '1,2,apple,banana,', 'expected' => array( '1', '2', 'apple', 'banana' ), ), - 'space before comma' => array( + 'space before comma' => array( 'input_list' => '1,2 ,apple,banana', 'expected' => array( '1', '2', 'apple', 'banana' ), ), - 'empty string' => array( + 'empty string' => array( 'input_list' => '', 'expected' => array(), ), - 'comma only' => array( + 'comma only' => array( 'input_list' => ',', 'expected' => array(), ), - 'double comma only' => array( + 'double comma only' => array( 'input_list' => ',,', 'expected' => array(), ), + 'passed scalar array' => array( + 'input_list' => array( 'foo', true, false, 1, 3.14 ), + 'expected' => array( 'foo', true, false, 1, 3.14 ), + ), + 'passed mixed array' => array( + 'input_list' => array( null, 'foo', array(), true, new stdClass(), false, 1, 3.14 ), + 'expected' => array( 'foo', true, false, 1, 3.14 ), + ), ); } } From c14e8fd8d3bc793f634384fd5ee177a17bedcb1b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:46:54 -0700 Subject: [PATCH 09/22] Improve Tests_Functions_wpParseIdList --- .../phpunit/tests/functions/wpParseIdList.php | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/functions/wpParseIdList.php b/tests/phpunit/tests/functions/wpParseIdList.php index cd1af1e923166..e1b582e4ab341 100644 --- a/tests/phpunit/tests/functions/wpParseIdList.php +++ b/tests/phpunit/tests/functions/wpParseIdList.php @@ -15,17 +15,32 @@ class Tests_Functions_wpParseIdList extends WP_UnitTestCase { * * @dataProvider data_wp_parse_id_list * @dataProvider data_unexpected_input + * + * @param mixed[]|string $input_list + * @param list $expected */ - public function test_wp_parse_id_list( $input_list, $expected ) { - $this->assertSameSets( $expected, wp_parse_id_list( $input_list ) ); + public function test_wp_parse_id_list( $input_list, $expected ): void { + $parsed_list = wp_parse_id_list( $input_list ); + $this->assertTrue( array_is_list( $parsed_list ), 'Expected value to be a list.' ); + $this->assertThat( + $parsed_list, + $this->callback( + static fn ( array $arr ): bool => array_all( + $arr, + static fn( $v ) => is_int( $v ) && $v >= 0 + ) + ), + 'Array should contain only non-negative ints.' + ); + $this->assertSameSets( $expected, $parsed_list ); } /** * Data provider. * - * @return array[] + * @return array }> */ - public function data_wp_parse_id_list() { + public function data_wp_parse_id_list(): array { return array( 'regular' => array( 'input_list' => '1,2,3,4', @@ -61,9 +76,9 @@ public function data_wp_parse_id_list() { /** * Data provider. * - * @return array[] + * @return array }> */ - public function data_unexpected_input() { + public function data_unexpected_input(): array { return array( 'string with commas' => array( 'input_list' => '1,2,string with spaces', @@ -97,6 +112,10 @@ public function data_unexpected_input() { 'input_list' => array( 1, 2, false ), 'expected' => array( 1, 2, 0 ), ), + 'array with array' => array( + 'input_list' => array( 1, array(), 2 ), + 'expected' => array( 1, 2 ), + ), ); } } From e7c8fe0c534b64e941d8470c028eb75bad4af8ba Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:48:59 -0700 Subject: [PATCH 10/22] Use assertSame instead of assertSameSets --- tests/phpunit/tests/functions/wpParseIdList.php | 2 +- tests/phpunit/tests/functions/wpParseList.php | 2 +- tests/phpunit/tests/functions/wpParseSlugList.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/functions/wpParseIdList.php b/tests/phpunit/tests/functions/wpParseIdList.php index e1b582e4ab341..9b57d9312de09 100644 --- a/tests/phpunit/tests/functions/wpParseIdList.php +++ b/tests/phpunit/tests/functions/wpParseIdList.php @@ -32,7 +32,7 @@ public function test_wp_parse_id_list( $input_list, $expected ): void { ), 'Array should contain only non-negative ints.' ); - $this->assertSameSets( $expected, $parsed_list ); + $this->assertSame( $expected, $parsed_list ); } /** diff --git a/tests/phpunit/tests/functions/wpParseList.php b/tests/phpunit/tests/functions/wpParseList.php index fef1787becf7c..94cf32cb4c62b 100644 --- a/tests/phpunit/tests/functions/wpParseList.php +++ b/tests/phpunit/tests/functions/wpParseList.php @@ -20,7 +20,7 @@ class Tests_Functions_wpParseList extends WP_UnitTestCase { public function test_wp_parse_list( $input_list, array $expected ): void { $parsed_list = wp_parse_list( $input_list ); $this->assertTrue( array_is_list( $parsed_list ), 'Expected value to be a list.' ); - $this->assertSameSets( $expected, $parsed_list ); + $this->assertSame( $expected, $parsed_list ); } /** diff --git a/tests/phpunit/tests/functions/wpParseSlugList.php b/tests/phpunit/tests/functions/wpParseSlugList.php index 8b9ccb2ddb091..0374fb847fc3d 100644 --- a/tests/phpunit/tests/functions/wpParseSlugList.php +++ b/tests/phpunit/tests/functions/wpParseSlugList.php @@ -17,7 +17,7 @@ class Tests_Functions_WpParseSlugList extends WP_UnitTestCase { * @dataProvider data_unexpected_input */ public function test_wp_parse_slug_list( $input_list, $expected ) { - $this->assertSameSets( $expected, wp_parse_slug_list( $input_list ) ); + $this->assertSame( $expected, wp_parse_slug_list( $input_list ) ); } /** From 6900fc5ca21c5e911cdc2dc888b67fc3436fbd4c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:49:18 -0700 Subject: [PATCH 11/22] Fix type of list --- tests/phpunit/tests/functions/wpParseIdList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/functions/wpParseIdList.php b/tests/phpunit/tests/functions/wpParseIdList.php index 9b57d9312de09..5a00e8c018965 100644 --- a/tests/phpunit/tests/functions/wpParseIdList.php +++ b/tests/phpunit/tests/functions/wpParseIdList.php @@ -17,7 +17,7 @@ class Tests_Functions_wpParseIdList extends WP_UnitTestCase { * @dataProvider data_unexpected_input * * @param mixed[]|string $input_list - * @param list $expected + * @param list $expected */ public function test_wp_parse_id_list( $input_list, $expected ): void { $parsed_list = wp_parse_id_list( $input_list ); From 0838a96487063e7e3ec6e2181602c4e53caa78aa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 18:57:30 -0700 Subject: [PATCH 12/22] Update tests for wp_parse_slug_list() --- .../phpunit/tests/functions/wpParseIdList.php | 6 ++-- .../tests/functions/wpParseSlugList.php | 35 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/tests/phpunit/tests/functions/wpParseIdList.php b/tests/phpunit/tests/functions/wpParseIdList.php index 5a00e8c018965..81c3c4bde0e26 100644 --- a/tests/phpunit/tests/functions/wpParseIdList.php +++ b/tests/phpunit/tests/functions/wpParseIdList.php @@ -19,15 +19,15 @@ class Tests_Functions_wpParseIdList extends WP_UnitTestCase { * @param mixed[]|string $input_list * @param list $expected */ - public function test_wp_parse_id_list( $input_list, $expected ): void { + public function test_wp_parse_id_list( $input_list, array $expected ): void { $parsed_list = wp_parse_id_list( $input_list ); $this->assertTrue( array_is_list( $parsed_list ), 'Expected value to be a list.' ); $this->assertThat( $parsed_list, $this->callback( - static fn ( array $arr ): bool => array_all( + static fn ( array $arr ) => array_all( $arr, - static fn( $v ) => is_int( $v ) && $v >= 0 + static fn ( $v ) => is_int( $v ) && $v >= 0 ) ), 'Array should contain only non-negative ints.' diff --git a/tests/phpunit/tests/functions/wpParseSlugList.php b/tests/phpunit/tests/functions/wpParseSlugList.php index 0374fb847fc3d..b6c23fb97a864 100644 --- a/tests/phpunit/tests/functions/wpParseSlugList.php +++ b/tests/phpunit/tests/functions/wpParseSlugList.php @@ -15,17 +15,32 @@ class Tests_Functions_WpParseSlugList extends WP_UnitTestCase { * * @dataProvider data_wp_parse_slug_list * @dataProvider data_unexpected_input + * + * @param mixed[]|string $input_list + * @param list $expected */ - public function test_wp_parse_slug_list( $input_list, $expected ) { - $this->assertSame( $expected, wp_parse_slug_list( $input_list ) ); + public function test_wp_parse_slug_list( $input_list, array $expected ): void { + $parsed_list = wp_parse_slug_list( $input_list ); + $this->assertTrue( array_is_list( $parsed_list ), 'Expected value to be a list.' ); + $this->assertThat( + $parsed_list, + $this->callback( + static fn ( array $arr ) => array_all( + $arr, + static fn ( $v ) => is_string( $v ) + ) + ), + 'Array should contain only non-negative ints.' + ); + $this->assertSame( $expected, $parsed_list ); } /** * Data provider. * - * @return array[] + * @return array }> */ - public function data_wp_parse_slug_list() { + public function data_wp_parse_slug_list(): array { return array( 'regular' => array( 'input_list' => 'apple,banana,carrot,dog', @@ -57,9 +72,9 @@ public function data_wp_parse_slug_list() { /** * Data provider. * - * @return array[] + * @return array }> */ - public function data_unexpected_input() { + public function data_unexpected_input(): array { return array( 'string with commas' => array( 'input_list' => '1,2,string with spaces', @@ -93,6 +108,14 @@ public function data_unexpected_input() { 'input_list' => array( 1, 2, false ), 'expected' => array( '1', '2', '' ), ), + 'array with array' => array( + 'input_list' => array( 1, array(), 2 ), + 'expected' => array( '1', '2' ), + ), + 'array with tag' => array( + 'input_list' => array( 1, '
', 2 ), + 'expected' => array( '1', '', '2' ), + ), ); } } From 3fbcfeafffa98ec6d3663b582959e8e9450f38f0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 19:12:32 -0700 Subject: [PATCH 13/22] Add tests for assoc arrays being made into lists --- tests/phpunit/tests/functions/wpParseIdList.php | 8 ++++++++ tests/phpunit/tests/functions/wpParseList.php | 8 ++++++++ tests/phpunit/tests/functions/wpParseSlugList.php | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/tests/phpunit/tests/functions/wpParseIdList.php b/tests/phpunit/tests/functions/wpParseIdList.php index 81c3c4bde0e26..c513c44351724 100644 --- a/tests/phpunit/tests/functions/wpParseIdList.php +++ b/tests/phpunit/tests/functions/wpParseIdList.php @@ -116,6 +116,14 @@ public function data_unexpected_input(): array { 'input_list' => array( 1, array(), 2 ), 'expected' => array( 1, 2 ), ), + 'passed assoc array' => array( + 'input_list' => array( + 'one' => 1, + 'two' => '2', + 'three' => '3 is company', + ), + 'expected' => array( 1, 2, 3 ), + ), ); } } diff --git a/tests/phpunit/tests/functions/wpParseList.php b/tests/phpunit/tests/functions/wpParseList.php index 94cf32cb4c62b..973156a63e854 100644 --- a/tests/phpunit/tests/functions/wpParseList.php +++ b/tests/phpunit/tests/functions/wpParseList.php @@ -82,6 +82,14 @@ public function data_wp_parse_list(): array { 'input_list' => array( null, 'foo', array(), true, new stdClass(), false, 1, 3.14 ), 'expected' => array( 'foo', true, false, 1, 3.14 ), ), + 'passed assoc array' => array( + 'input_list' => array( + 'foo' => 1, + 'bar' => true, + 'baz' => 3.14, + ), + 'expected' => array( 1, true, 3.14 ), + ), ); } } diff --git a/tests/phpunit/tests/functions/wpParseSlugList.php b/tests/phpunit/tests/functions/wpParseSlugList.php index b6c23fb97a864..a3fdb0e162884 100644 --- a/tests/phpunit/tests/functions/wpParseSlugList.php +++ b/tests/phpunit/tests/functions/wpParseSlugList.php @@ -116,6 +116,14 @@ public function data_unexpected_input(): array { 'input_list' => array( 1, '
', 2 ), 'expected' => array( '1', '', '2' ), ), + 'passed assoc array' => array( + 'input_list' => array( + 'one' => 'foo', + 'two' => 'bar', + 'three' => 'baz', + ), + 'expected' => array( 'foo', 'bar', 'baz' ), + ), ); } } From 70db2cf08081a7d2f05199ebcfbc406f287a2d7c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 19:30:55 -0700 Subject: [PATCH 14/22] Abandon coercing to lists --- src/wp-includes/functions.php | 35 +++++++------- .../phpunit/tests/functions/wpParseIdList.php | 25 +++++++--- tests/phpunit/tests/functions/wpParseList.php | 19 ++++++-- .../tests/functions/wpParseSlugList.php | 46 +++++++++++++------ 4 files changed, 79 insertions(+), 46 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index cc40408f53d94..67d50de4b977e 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5022,8 +5022,8 @@ function wp_parse_args( $args, $defaults = array() ) { * @since 5.1.0 * * @param mixed[]|string $input_list List of values. - * @return array Array of values. - * @phpstan-return list + * @return array Array of values. Not guaranteed to be a PHP list. + * @phpstan-return array */ function wp_parse_list( $input_list ) { if ( ! is_array( $input_list ) ) { @@ -5031,7 +5031,7 @@ function wp_parse_list( $input_list ) { } // Validate all entries of the list are scalar. - $input_list = array_values( array_filter( $input_list, 'is_scalar' ) ); + $input_list = array_filter( $input_list, 'is_scalar' ); return $input_list; } @@ -5043,13 +5043,13 @@ function wp_parse_list( $input_list ) { * @since 5.1.0 Refactored to use wp_parse_list(). * * @param mixed[]|string $input_list List of IDs. - * @return int[] Sanitized array of IDs. May include zero. - * @phpstan-return list + * @return int[] Sanitized array of IDs. May include zero. Not guaranteed to be a PHP list. + * @phpstan-return array */ function wp_parse_id_list( $input_list ) { $input_list = wp_parse_list( $input_list ); - return array_values( array_unique( array_map( 'absint', $input_list ) ) ); + return array_unique( array_map( 'absint', $input_list ) ); } /** @@ -5059,24 +5059,21 @@ function wp_parse_id_list( $input_list ) { * @since 5.1.0 Refactored to use wp_parse_list(). * * @param mixed[]|string $input_list List of slugs. - * @return string[] Sanitized array of slugs. May include an empty string. - * @phpstan-return list + * @return string[] Sanitized array of slugs. May include an empty string. Not guaranteed to be a PHP list. */ function wp_parse_slug_list( $input_list ) { $input_list = wp_parse_list( $input_list ); - return array_values( - array_unique( + return array_unique( + array_map( + 'sanitize_title', array_map( - 'sanitize_title', - array_map( - /* - * Cast booleans, integers, and floats to strings. Non-scalar types (including null) have already - * been filtered out by wp_parse_list(). - */ - 'strval', - $input_list - ) + /* + * Cast booleans, integers, and floats to strings. Non-scalar types (including null) have already + * been filtered out by wp_parse_list(). + */ + 'strval', + $input_list ) ) ); diff --git a/tests/phpunit/tests/functions/wpParseIdList.php b/tests/phpunit/tests/functions/wpParseIdList.php index c513c44351724..326fc2d2adffb 100644 --- a/tests/phpunit/tests/functions/wpParseIdList.php +++ b/tests/phpunit/tests/functions/wpParseIdList.php @@ -17,11 +17,10 @@ class Tests_Functions_wpParseIdList extends WP_UnitTestCase { * @dataProvider data_unexpected_input * * @param mixed[]|string $input_list - * @param list $expected + * @param array $expected */ public function test_wp_parse_id_list( $input_list, array $expected ): void { $parsed_list = wp_parse_id_list( $input_list ); - $this->assertTrue( array_is_list( $parsed_list ), 'Expected value to be a list.' ); $this->assertThat( $parsed_list, $this->callback( @@ -38,7 +37,7 @@ public function test_wp_parse_id_list( $input_list, array $expected ): void { /** * Data provider. * - * @return array }> + * @return array }> */ public function data_wp_parse_id_list(): array { return array( @@ -52,7 +51,12 @@ public function data_wp_parse_id_list(): array { ), 'duplicate id in a string' => array( 'input_list' => '1,2,2,3,4', - 'expected' => array( 1, 2, 3, 4 ), + 'expected' => array( + 0 => 1, + 1 => 2, + 3 => 3, + 4 => 4, + ), ), 'duplicate id in an array' => array( 'input_list' => array( '1', '2', '3', '4', '3' ), @@ -76,7 +80,7 @@ public function data_wp_parse_id_list(): array { /** * Data provider. * - * @return array }> + * @return array }> */ public function data_unexpected_input(): array { return array( @@ -114,7 +118,10 @@ public function data_unexpected_input(): array { ), 'array with array' => array( 'input_list' => array( 1, array(), 2 ), - 'expected' => array( 1, 2 ), + 'expected' => array( + 0 => 1, + 2 => 2, + ), ), 'passed assoc array' => array( 'input_list' => array( @@ -122,7 +129,11 @@ public function data_unexpected_input(): array { 'two' => '2', 'three' => '3 is company', ), - 'expected' => array( 1, 2, 3 ), + 'expected' => array( + 'one' => 1, + 'two' => 2, + 'three' => 3, + ), ), ); } diff --git a/tests/phpunit/tests/functions/wpParseList.php b/tests/phpunit/tests/functions/wpParseList.php index 973156a63e854..fc548a3dbe69b 100644 --- a/tests/phpunit/tests/functions/wpParseList.php +++ b/tests/phpunit/tests/functions/wpParseList.php @@ -15,18 +15,17 @@ class Tests_Functions_wpParseList extends WP_UnitTestCase { * @dataProvider data_wp_parse_list * * @param mixed[]|string $input_list - * @param list $expected + * @param array $expected */ public function test_wp_parse_list( $input_list, array $expected ): void { $parsed_list = wp_parse_list( $input_list ); - $this->assertTrue( array_is_list( $parsed_list ), 'Expected value to be a list.' ); $this->assertSame( $expected, $parsed_list ); } /** * Data provider. * - * @return array }> + * @return array }> */ public function data_wp_parse_list(): array { return array( @@ -80,7 +79,13 @@ public function data_wp_parse_list(): array { ), 'passed mixed array' => array( 'input_list' => array( null, 'foo', array(), true, new stdClass(), false, 1, 3.14 ), - 'expected' => array( 'foo', true, false, 1, 3.14 ), + 'expected' => array( + 1 => 'foo', + 3 => true, + 5 => false, + 6 => 1, + 7 => 3.14, + ), ), 'passed assoc array' => array( 'input_list' => array( @@ -88,7 +93,11 @@ public function data_wp_parse_list(): array { 'bar' => true, 'baz' => 3.14, ), - 'expected' => array( 1, true, 3.14 ), + 'expected' => array( + 'foo' => 1, + 'bar' => true, + 'baz' => 3.14, + ), ), ); } diff --git a/tests/phpunit/tests/functions/wpParseSlugList.php b/tests/phpunit/tests/functions/wpParseSlugList.php index a3fdb0e162884..af3a08f014ba2 100644 --- a/tests/phpunit/tests/functions/wpParseSlugList.php +++ b/tests/phpunit/tests/functions/wpParseSlugList.php @@ -17,11 +17,10 @@ class Tests_Functions_WpParseSlugList extends WP_UnitTestCase { * @dataProvider data_unexpected_input * * @param mixed[]|string $input_list - * @param list $expected + * @param array $expected */ public function test_wp_parse_slug_list( $input_list, array $expected ): void { $parsed_list = wp_parse_slug_list( $input_list ); - $this->assertTrue( array_is_list( $parsed_list ), 'Expected value to be a list.' ); $this->assertThat( $parsed_list, $this->callback( @@ -38,7 +37,7 @@ public function test_wp_parse_slug_list( $input_list, array $expected ): void { /** * Data provider. * - * @return array }> + * @return array }> */ public function data_wp_parse_slug_list(): array { return array( @@ -52,11 +51,21 @@ public function data_wp_parse_slug_list(): array { ), 'duplicate slug in a string' => array( 'input_list' => 'apple,banana,carrot,carrot,dog', - 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), + 'expected' => array( + 0 => 'apple', + 1 => 'banana', + 2 => 'carrot', + 4 => 'dog', + ), ), 'duplicate slug in an array' => array( 'input_list' => array( 'apple', 'banana', 'carrot', 'carrot', 'dog' ), - 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), + 'expected' => array( + 0 => 'apple', + 1 => 'banana', + 2 => 'carrot', + 4 => 'dog', + ), ), 'string with spaces' => array( 'input_list' => 'apple banana carrot dog', @@ -66,13 +75,25 @@ public function data_wp_parse_slug_list(): array { 'input_list' => array( 'apple ', 'banana carrot', 'd o g' ), 'expected' => array( 'apple', 'banana-carrot', 'd-o-g' ), ), + 'passed assoc array' => array( + 'input_list' => array( + 'one' => 'foo', + 'two' => 'bar', + 'three' => 'baz', + ), + 'expected' => array( + 'one' => 'foo', + 'two' => 'bar', + 'three' => 'baz', + ), + ), ); } /** * Data provider. * - * @return array }> + * @return array }> */ public function data_unexpected_input(): array { return array( @@ -110,20 +131,15 @@ public function data_unexpected_input(): array { ), 'array with array' => array( 'input_list' => array( 1, array(), 2 ), - 'expected' => array( '1', '2' ), + 'expected' => array( + 0 => '1', + 2 => '2', + ), ), 'array with tag' => array( 'input_list' => array( 1, '
', 2 ), 'expected' => array( '1', '', '2' ), ), - 'passed assoc array' => array( - 'input_list' => array( - 'one' => 'foo', - 'two' => 'bar', - 'three' => 'baz', - ), - 'expected' => array( 'foo', 'bar', 'baz' ), - ), ); } } From 150c52d56f9db1eb910a2285031085b3a7816537 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 20:15:34 -0700 Subject: [PATCH 15/22] Avoid casting preg_split() failure to array( false ) 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 --- src/wp-includes/functions.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 67d50de4b977e..1cd24bab5ca99 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5027,7 +5027,8 @@ function wp_parse_args( $args, $defaults = array() ) { */ function wp_parse_list( $input_list ) { if ( ! is_array( $input_list ) ) { - return (array) preg_split( '/[\s,]+/', $input_list, -1, PREG_SPLIT_NO_EMPTY ); + $parsed_list = preg_split( '/[\s,]+/', $input_list, -1, PREG_SPLIT_NO_EMPTY ); + return is_array( $parsed_list ) ? $parsed_list : array(); } // Validate all entries of the list are scalar. From 109089dd54727171fc82c3aa05801cdf9c10339f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 20:17:21 -0700 Subject: [PATCH 16/22] Correct copy-pasted assertion message in wp_parse_slug_list() test 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 --- tests/phpunit/tests/functions/wpParseSlugList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/functions/wpParseSlugList.php b/tests/phpunit/tests/functions/wpParseSlugList.php index af3a08f014ba2..d103bab4309b8 100644 --- a/tests/phpunit/tests/functions/wpParseSlugList.php +++ b/tests/phpunit/tests/functions/wpParseSlugList.php @@ -29,7 +29,7 @@ public function test_wp_parse_slug_list( $input_list, array $expected ): void { static fn ( $v ) => is_string( $v ) ) ), - 'Array should contain only non-negative ints.' + 'Array should contain only strings.' ); $this->assertSame( $expected, $parsed_list ); } From c4ed42f507e549c9518f7c7505a1e0082f11d22c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 20:22:38 -0700 Subject: [PATCH 17/22] Assert the scalar return contract in the wp_parse_list() test 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 --- tests/phpunit/tests/functions/wpParseList.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/phpunit/tests/functions/wpParseList.php b/tests/phpunit/tests/functions/wpParseList.php index fc548a3dbe69b..e7bcce248ce85 100644 --- a/tests/phpunit/tests/functions/wpParseList.php +++ b/tests/phpunit/tests/functions/wpParseList.php @@ -19,6 +19,16 @@ class Tests_Functions_wpParseList extends WP_UnitTestCase { */ public function test_wp_parse_list( $input_list, array $expected ): void { $parsed_list = wp_parse_list( $input_list ); + $this->assertThat( + $parsed_list, + $this->callback( + static fn ( array $arr ) => array_all( + $arr, + static fn ( $v ) => is_scalar( $v ) + ) + ), + 'Array should contain only scalars.' + ); $this->assertSame( $expected, $parsed_list ); } From 04257fb9c8ed484ac7ae476191f2f26af71a462c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 20:25:37 -0700 Subject: [PATCH 18/22] Document why the parsed lists are not necessarily PHP lists 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 --- src/wp-includes/functions.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 1cd24bab5ca99..f5dfe1fa5d836 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5022,7 +5022,8 @@ function wp_parse_args( $args, $defaults = array() ) { * @since 5.1.0 * * @param mixed[]|string $input_list List of values. - * @return array Array of values. Not guaranteed to be a PHP list. + * @return array Array of values. Keys are preserved when an array is passed, + * so the result is not necessarily a list. * @phpstan-return array */ function wp_parse_list( $input_list ) { @@ -5044,7 +5045,9 @@ function wp_parse_list( $input_list ) { * @since 5.1.0 Refactored to use wp_parse_list(). * * @param mixed[]|string $input_list List of IDs. - * @return int[] Sanitized array of IDs. May include zero. Not guaranteed to be a PHP list. + * @return int[] Sanitized array of IDs. May include zero. Keys are preserved + * from the input and `array_unique()` may leave gaps, so the + * result is not necessarily a list. * @phpstan-return array */ function wp_parse_id_list( $input_list ) { @@ -5060,7 +5063,9 @@ function wp_parse_id_list( $input_list ) { * @since 5.1.0 Refactored to use wp_parse_list(). * * @param mixed[]|string $input_list List of slugs. - * @return string[] Sanitized array of slugs. May include an empty string. Not guaranteed to be a PHP list. + * @return string[] Sanitized array of slugs. May include an empty string. Keys + * are preserved from the input and `array_unique()` may leave + * gaps, so the result is not necessarily a list. */ function wp_parse_slug_list( $input_list ) { $input_list = wp_parse_list( $input_list ); From c92aa4a6fadb17e1b800fc2c440110200d9d153e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 20:32:03 -0700 Subject: [PATCH 19/22] Narrow wp_parse_list() to a list when a string is passed 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 --- src/wp-includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index f5dfe1fa5d836..6deeb9b871448 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5024,7 +5024,7 @@ function wp_parse_args( $args, $defaults = array() ) { * @param mixed[]|string $input_list List of values. * @return array Array of values. Keys are preserved when an array is passed, * so the result is not necessarily a list. - * @phpstan-return array + * @phpstan-return ($input_list is string ? list : array) */ function wp_parse_list( $input_list ) { if ( ! is_array( $input_list ) ) { From c493b6f58566cec684d42746678380485c691513 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 18 Jul 2026 20:34:41 -0700 Subject: [PATCH 20/22] State the string-yields-a-list guarantee in the prose return description 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 --- src/wp-includes/functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 6deeb9b871448..91d52bc68f22a 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5022,8 +5022,8 @@ function wp_parse_args( $args, $defaults = array() ) { * @since 5.1.0 * * @param mixed[]|string $input_list List of values. - * @return array Array of values. Keys are preserved when an array is passed, - * so the result is not necessarily a list. + * @return array Array of values. A string is split into a list, while an array + * keeps its keys, so the result is not necessarily a list. * @phpstan-return ($input_list is string ? list : array) */ function wp_parse_list( $input_list ) { From 5b0c4884ada1d22f2619cf07d59e76e9e9eaa2a4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 19 Jul 2026 09:27:27 -0700 Subject: [PATCH 21/22] Reduce line length --- src/wp-includes/functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 91d52bc68f22a..393339d1a4f59 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5075,8 +5075,8 @@ function wp_parse_slug_list( $input_list ) { 'sanitize_title', array_map( /* - * Cast booleans, integers, and floats to strings. Non-scalar types (including null) have already - * been filtered out by wp_parse_list(). + * Cast booleans, integers, and floats to strings. Non-scalar types + * (including null) have already been filtered out by wp_parse_list(). */ 'strval', $input_list From 03c79b0c59745831d9bac08f3505638f16e86b3c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 19 Jul 2026 09:43:06 -0700 Subject: [PATCH 22/22] Add return type hints --- src/wp-includes/functions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 393339d1a4f59..c6ee657d2abc5 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5026,7 +5026,7 @@ function wp_parse_args( $args, $defaults = array() ) { * keeps its keys, so the result is not necessarily a list. * @phpstan-return ($input_list is string ? list : array) */ -function wp_parse_list( $input_list ) { +function wp_parse_list( $input_list ): array { if ( ! is_array( $input_list ) ) { $parsed_list = preg_split( '/[\s,]+/', $input_list, -1, PREG_SPLIT_NO_EMPTY ); return is_array( $parsed_list ) ? $parsed_list : array(); @@ -5050,7 +5050,7 @@ function wp_parse_list( $input_list ) { * result is not necessarily a list. * @phpstan-return array */ -function wp_parse_id_list( $input_list ) { +function wp_parse_id_list( $input_list ): array { $input_list = wp_parse_list( $input_list ); return array_unique( array_map( 'absint', $input_list ) ); @@ -5067,7 +5067,7 @@ function wp_parse_id_list( $input_list ) { * are preserved from the input and `array_unique()` may leave * gaps, so the result is not necessarily a list. */ -function wp_parse_slug_list( $input_list ) { +function wp_parse_slug_list( $input_list ): array { $input_list = wp_parse_list( $input_list ); return array_unique(