From 88a1415008e0308505c9a99a1e7b2bff8fdae04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Sz=C3=A9pe?= Date: Tue, 20 Oct 2026 09:25:49 +0000 Subject: [PATCH 1/5] Preserve existing phpstan-return tags when applying function map --- src/Visitor.php | 11 +++++++++++ tests/VisitorTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/Visitor.php b/src/Visitor.php index f15fc5a..b99b991 100644 --- a/src/Visitor.php +++ b/src/Visitor.php @@ -552,6 +552,17 @@ private function addStringTags(string $name, Doc $docComment): ?Doc $additions = $this->additionalTagStrings[$name]; $docCommentText = $docComment->getText(); + if (preg_match('/(?:^\h*\*|\/\*\*)\h*@phpstan-return\b/m', $docCommentText) === 1) { + $additions = array_filter( + $additions, + static fn (string $tag): bool => ! str_starts_with($tag, '@phpstan-return ') + ); + } + + if (count($additions) === 0) { + return null; + } + $newDocComment = sprintf( "%s\n * %s\n */", substr($docCommentText, 0, -self::WP_INDENT_SIZE), diff --git a/tests/VisitorTest.php b/tests/VisitorTest.php index a36c7b5..4cac654 100644 --- a/tests/VisitorTest.php +++ b/tests/VisitorTest.php @@ -67,6 +67,33 @@ function wpstubs_test_connector_with_phpstan_return( string $id ): ?array { ); } + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testMapReturnDoesNotOverrideExistingReturn(): void + { + $code = <<<'PHP' + Date: Tue, 20 Oct 2026 09:28:16 +0000 Subject: [PATCH 2/5] Use docblock parser to detect existing return tags --- src/Visitor.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Visitor.php b/src/Visitor.php index b99b991..16cf16f 100644 --- a/src/Visitor.php +++ b/src/Visitor.php @@ -552,7 +552,13 @@ private function addStringTags(string $name, Doc $docComment): ?Doc $additions = $this->additionalTagStrings[$name]; $docCommentText = $docComment->getText(); - if (preg_match('/(?:^\h*\*|\/\*\*)\h*@phpstan-return\b/m', $docCommentText) === 1) { + try { + $docblock = $this->docBlockFactory->create($docCommentText); + } catch (\RuntimeException | \InvalidArgumentException $e) { + return null; + } + + if (count($docblock->getTagsByName('phpstan-return')) > 0) { $additions = array_filter( $additions, static fn (string $tag): bool => ! str_starts_with($tag, '@phpstan-return ') From c19dc8bc8a55e1cff65d32105cf24a3c329df32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Sz=C3=A9pe?= Date: Tue, 20 Oct 2026 09:40:04 +0000 Subject: [PATCH 3/5] Regenerate stubs without duplicate mapped return tags --- src/Visitor.php | 4 +++- wordpress-stubs.php | 15 --------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Visitor.php b/src/Visitor.php index 16cf16f..a6dd986 100644 --- a/src/Visitor.php +++ b/src/Visitor.php @@ -561,7 +561,9 @@ private function addStringTags(string $name, Doc $docComment): ?Doc if (count($docblock->getTagsByName('phpstan-return')) > 0) { $additions = array_filter( $additions, - static fn (string $tag): bool => ! str_starts_with($tag, '@phpstan-return ') + static function (string $tag): bool { + return ! str_starts_with($tag, '@phpstan-return '); + } ); } diff --git a/wordpress-stubs.php b/wordpress-stubs.php index 9565127..c924d65 100644 --- a/wordpress-stubs.php +++ b/wordpress-stubs.php @@ -45004,7 +45004,6 @@ class WP_Http * filename: string|null, * http_response: WP_HTTP_Requests_Response|null, * } - * @phpstan-return array{headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary, body: string, response: array{code: int, message: string}, cookies: array, filename: string|null, http_response: \WP_HTTP_Requests_Response}|\WP_Error */ public function request($url, $args = array()) { @@ -50965,7 +50964,6 @@ public function rewind_comments() * @phpstan-return ( * $query is array{ fields: 'ids', ... } ? int[] : WP_Post[] * ) - * @phpstan-return ($query is array ? ($query is array{fields: 'id=>parent'|'ids', ...} ? array : array) : array|array) */ public function query($query) { @@ -130545,7 +130543,6 @@ function stripslashes_deep($value) * @phpstan-template T * @phpstan-param T|string $value * @phpstan-pure - * @phpstan-return ($value is string ? ($value is '' ? '' : string) : T) */ function stripslashes_from_strings_only($value) { @@ -132779,7 +132776,6 @@ function win_is_writable($path) * baseurl: non-empty-string, * } * |array{ error: non-empty-string } - * @phpstan-return array{path: string, url: string, subdir: string, basedir: string, baseurl: string, error: false|string} */ function wp_get_upload_dir() { @@ -132828,7 +132824,6 @@ function wp_get_upload_dir() * baseurl: non-empty-string, * } * |array{ error: non-empty-string } - * @phpstan-return array{path: string, url: string, subdir: string, basedir: string, baseurl: string, error: false|string} */ function wp_upload_dir($time = \null, $create_dir = \true, $refresh_cache = \false) { @@ -133158,7 +133153,6 @@ function wp_nonce_ays($action) * code?: string, * exit?: bool, * } $args - * @phpstan-return ($args is array{exit: false, ...} ? void : never) */ function wp_die($message = '', $title = '', $args = array()) { @@ -150588,7 +150582,6 @@ function auth_redirect() * as the function otherwise exits rather than returning false. * @phpstan-return ( $action is -1 ? int|false : int ) * @phpstan-param string $action - * @phpstan-return ($action is -1 ? 1|2|false : 1|2) */ function check_admin_referer($action = -1, $query_arg = '_wpnonce') { @@ -150610,7 +150603,6 @@ function check_admin_referer($action = -1, $query_arg = '_wpnonce') * as the function otherwise exits rather than returning false. * @phpstan-return ( $stop is true ? int : int|false ) * @phpstan-param string $action - * @phpstan-return ($stop is true ? 1|2 : 1|2|false) */ function check_ajax_referer($action = -1, $query_arg = \false, $stop = \true) { @@ -153294,7 +153286,6 @@ function get_post_type_object($post_type) * @return string[]|WP_Post_Type[] An array of post type names or objects. * @phpstan-return ( $output is 'names' ? string[] : WP_Post_Type[] ) * @phpstan-param 'names'|'objects' $output - * @phpstan-return ($output is 'names' ? array : array) */ function get_post_types($args = array(), $output = 'names', $operator = 'and') { @@ -154025,7 +154016,6 @@ function is_post_embeddable($post = \null) * w?: int, * year?: int, * } $args - * @phpstan-return ($args is array{fields: 'id=>parent'|'ids', ...} ? array : array) */ function get_posts($args = \null) { @@ -154298,7 +154288,6 @@ function sanitize_post($post, $context = 'display') * ) * @phpstan-template T of mixed * @phpstan-param T $value - * @phpstan-return ($field is 'ID'|'post_parent'|'menu_order' ? (T is int ? T : int) : ($field is 'ancestors' ? (T is array>|list> ? T : (T is list ? list> : array>)) : ($context is 'raw' ? T : ($context is 'attribute'|'js' ? string : ($context is 'edit' ? ($field is not 'post_content' ? string : mixed) : mixed))))) */ function sanitize_post_field($field, $value, $post_id, $context = 'display') { @@ -154823,7 +154812,6 @@ function wp_get_recent_posts($args = array(), $output = \ARRAY_A) * meta_input?: array, * page_template?: string, * } $postarr - * @phpstan-return ($wp_error is false ? int<0, max> : int<1, max>|\WP_Error) */ function wp_insert_post($postarr, $wp_error = \false, $fire_after_hooks = \true) { @@ -154876,7 +154864,6 @@ function wp_insert_post($postarr, $wp_error = \false, $fire_after_hooks = \true) * meta_input?: array, * page_template?: string, * } $postarr See wp_insert_post() - * @phpstan-return ($wp_error is false ? int<0, max> : int<1, max>|\WP_Error) */ function wp_update_post($postarr = array(), $wp_error = \false, $fire_after_hooks = \true) { @@ -155367,7 +155354,6 @@ function is_local_attachment($url) * @phpstan-return ( * $wp_error is false ? int : int|WP_Error * ) - * @phpstan-return ($wp_error is false ? int<0, max> : int<1, max>|\WP_Error) */ function wp_insert_attachment($args, $file = \false, $parent_post_id = 0, $wp_error = \false, $fire_after_hooks = \true) { @@ -161313,7 +161299,6 @@ function unregister_term_meta($taxonomy, $meta_key) * ) * ) * ) - * @phpstan-return ($term is 0 ? 0 : ($term is '' ? null : ($taxonomy is '' ? string|null : array{term_id: string, term_taxonomy_id: string}|null))) */ function term_exists($term, $taxonomy = '', $parent_term = \null) { From 91563732bab4ed2d798679f8b5918ef4727b8f86 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 20 Oct 2026 13:08:41 +0200 Subject: [PATCH 4/5] Fix duplicate parameter and template tags and shapes of generic arrays Map parameters replace the ones generated from the hash notation, and map parameters and templates do not duplicate the ones written in core, like map return types since #498. PHPStan doesn't support shapes of generic arrays (array{...}), so they become unsealed shapes (array{..., ...}), and the string type of args parameters is only removed from the top-level union. Lists of lists of shapes are nested generic arrays. Claude-Session: https://claude.ai/code/session_01WNSb29tptiTQoc44Tk7XZW --- functionMap.php | 3 +- src/Visitor.php | 128 ++++++++++++++---- src/WordPressTag.php | 77 +++++++++-- tests/ParameterTypeTest.php | 13 ++ tests/VisitorTest.php | 118 ++++++++++++++++ tests/data/param/wp-enqueue-script-module.php | 19 +++ tests/data/return/wp-dependencies.php | 3 + tests/data/return/wp-image-editor.php | 18 +++ 8 files changed, 346 insertions(+), 33 deletions(-) create mode 100644 tests/data/param/wp-enqueue-script-module.php create mode 100644 tests/data/return/wp-image-editor.php diff --git a/functionMap.php b/functionMap.php index 6fe36ba..acbcca6 100644 --- a/functionMap.php +++ b/functionMap.php @@ -361,11 +361,12 @@ 'WP_Object_Cache::delete' => [null, 'deprecated' => 'false'], 'WP_Dependencies::$groups' => [null, '@phpstan-var' => 'array'], 'WP_Dependencies::get_etag' => ['non-falsy-string'], - 'WP_Dependencies::query' => ["(\$handle is not non-empty-string ? false : (\$status is not TStatus ? false : (\$status is 'registered'|'scripts' ? _WP_Dependency|false : bool)))", '@phpstan-template TStatus' => "'registered'|'scripts'|'enqueued'|'queued'|'to_do'|'to_print'|'done'|'printed'"], + 'WP_Dependencies::query' => ["(\$handle is not non-empty-string ? false : (\$status is not TStatus ? false : (\$status is 'registered'|'scripts' ? _WP_Dependency|false : bool)))", '@phpstan-template TStatus' => "of 'registered'|'scripts'|'enqueued'|'queued'|'to_do'|'to_print'|'done'|'printed'"], 'WP_Http::get' => [$httpReturnType], 'WP_Http::head' => [$httpReturnType], 'WP_Http::post' => [$httpReturnType], 'WP_Http::request' => [$httpReturnType], + 'WP_Image_Editor::get_output_format' => ['array{string|null, string|false, string}'], 'WP_List_Table::set_pagination_args' => ['void', 'args' => 'array{total_items?: int, total_pages?: int, per_page?: int}'], 'WP_Locale::$word_count_type' => [null, '@phpstan-var' => "'characters_excluding_spaces'|'characters_including_spaces'|'words'"], 'WP_Locale::get_word_count_type' => ["'characters_excluding_spaces'|'characters_including_spaces'|'words'"], diff --git a/src/Visitor.php b/src/Visitor.php index a6dd986..affe37a 100644 --- a/src/Visitor.php +++ b/src/Visitor.php @@ -299,6 +299,51 @@ private function getVariableNamesFromTags(array $tags): array return $names; } + /** + * @param array<\phpDocumentor\Reflection\DocBlock\Tag> $tags + * @return list + */ + private static function getTemplateNamesFromTags(array $tags): array + { + $names = []; + + foreach ($tags as $tag) { + if (! ($tag instanceof Generic)) { + continue; + } + + if (preg_match('#^\s*(\S+)#', (string)$tag, $matches) !== 1) { + continue; + } + + $names[] = $matches[1]; + } + + return $names; + } + + /** + * @return list + */ + private function getParamNamesFromMap(string $symbolName): array + { + if (! array_key_exists($symbolName, $this->functionMap)) { + return []; + } + + $names = []; + + foreach (array_keys($this->functionMap[$symbolName]) as $paramName) { + if (! is_string($paramName) || str_starts_with($paramName, '@')) { + continue; + } + + $names[] = $paramName; + } + + return $names; + } + private function addTags(string $name, Doc $docComment): ?Doc { $additions = $this->additionalTags[$name] ?? []; @@ -311,6 +356,15 @@ private function addTags(string $name, Doc $docComment): ?Doc } $additions = $this->discoverInheritedArgs($docblock, $additions); + $mapParamNames = $this->getParamNamesFromMap($name); + + // A parameter type from the function map replaces the one generated from the hash notation. + $additions = array_filter( + $additions, + static function (WordPressTag $tag) use ($mapParamNames): bool { + return $tag->tag !== '@phpstan-param' || ! in_array($tag->name, $mapParamNames, true); + } + ); /** @var list $additionStrings */ $additionStrings = array_map( @@ -567,6 +621,24 @@ static function (string $tag): bool { ); } + $existingParamNames = $this->getVariableNamesFromTags($docblock->getTagsByName('phpstan-param')); + $existingTemplateNames = self::getTemplateNamesFromTags($docblock->getTagsByName('phpstan-template')); + + $additions = array_filter( + $additions, + static function (string $tag) use ($existingParamNames, $existingTemplateNames): bool { + if (preg_match('#^@phpstan-param\s.*\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)$#s', $tag, $matches) === 1) { + return ! in_array($matches[1], $existingParamNames, true); + } + + if (preg_match('#^@phpstan-template\s+(\S+)#', $tag, $matches) === 1) { + return ! in_array($matches[1], $existingTemplateNames, true); + } + + return true; + } + ); + if (count($additions) === 0) { return null; } @@ -616,7 +688,11 @@ private function getAdditionFromParam(Param $tag): ?WordPressTag // It's common for an args parameter to accept a query var string or array with `string|array`. // Remove the accepted string type for these so we get the strongest typing we can manage. - $tagVariableType = str_replace(['|string', 'string|'], '', $tagVariableType); + $members = WordPressTag::getUnionMembers($tagVariableType); + + if ($members !== null && count($members) > 1) { + $tagVariableType = implode('|', array_diff($members, ['string'])); + } $tag = new WordPressTag(); $tag->tag = '@phpstan-param'; @@ -753,37 +829,41 @@ private static function getTypeNameFromType(Type $tagVariableType): ?string private static function getTypeNameFromString(string $tagVariable): ?string { - // PHPStan doesn't support typed array shapes (`int[]{...}`) so replace - // typed arrays such as `int[]` with `array`. - $tagVariableType = preg_replace('#[a-zA-Z0-9_]+\[\]#', 'array', $tagVariable); + $members = WordPressTag::getUnionMembers($tagVariable); - if ($tagVariableType === null) { + if ($members === null) { return null; } - $tagVariableType = str_replace( - [ - 'stdClass', - '\\object', - ], - 'object', - $tagVariableType - ); + $members = array_map( + static function (string $member): string { + // PHPStan doesn't support typed array shapes (`int[]{...}`) so replace + // typed arrays such as `int[]` with `array`. + if (preg_match('#^[a-zA-Z0-9_\\\\]+(?:\[\])+$#', $member) === 1) { + return 'array'; + } - $supportedTypes = [ - 'object', - 'array', - ]; + if (in_array(ltrim($member, '\\'), ['stdClass', 'object'], true)) { + return 'object'; + } - foreach ($supportedTypes as $supportedType) { - if (! str_contains($tagVariableType, "{$supportedType}|")) { - continue; - } - // Move the type that uses the hash notation to the end of union types so the shape works. - $tagVariableType = sprintf('%s|%s', str_replace("{$supportedType}|", '', $tagVariableType), $supportedType); + return $member; + }, + $members + ); + + // Move the type that uses the hash notation to the end of union types so the shape works. + foreach (['object', 'array'] as $supportedType) { + $hashTypes = array_filter( + $members, + static function (string $member) use ($supportedType): bool { + return $member === $supportedType || str_starts_with($member, "{$supportedType}<"); + } + ); + $members = array_merge(array_diff_key($members, $hashTypes), $hashTypes); } - return $tagVariableType; + return implode('|', $members); } /** diff --git a/src/WordPressTag.php b/src/WordPressTag.php index 2d53a35..4e291aa 100644 --- a/src/WordPressTag.php +++ b/src/WordPressTag.php @@ -39,12 +39,18 @@ public function format(): array $strings = []; $childStrings = []; $level = 1; + $children = $this->children; + $listDepth = 0; if (! $this->isArrayShape()) { $level = 0; + $children = self::getListElementChildren($this, $listDepth); } - foreach ($this->children as $child) { + $listOpen = str_repeat('array', $listDepth); + + foreach ($children as $child) { $childStrings = array_merge($childStrings, $child->format($level)); } @@ -55,17 +61,28 @@ public function format(): array $name = $this->name !== null ? " \${$this->name}" : ''; if ($this->isArrayShape()) { + // PHPStan doesn't support shapes of generic arrays (`array{...}`), so turn + // `array` into an unsealed shape whose other elements are typed `K => V`. + $type = $this->type; + + if (preg_match('#^(?(?:.*\|)?)array(?<(?:[^<>]++|(?&generic))*>)$#s', $type, $matches) === 1) { + $type = "{$matches['union']}array"; + $childStrings[] = sprintf(' ...%s,', $matches['generic']); + } + $strings[] = sprintf( '%s %s{', $this->tag, - $this->type + $type ); } else { - if (! $this->children[0]->hasChildren()) { + if (! $children[0]->hasChildren()) { $strings[] = sprintf( - '%s array%s', + '%s array%s', $this->tag, - $this->type, + $listOpen, + $listDepth === 0 ? $this->type : $children[0]->type, + $listClose, $name ); @@ -73,9 +90,10 @@ public function format(): array } $strings[] = sprintf( - '%s %stag, - $this->type + $this->type, + $listOpen ); } @@ -88,8 +106,51 @@ public function format(): array $strings[] = $this->isArrayShape() ? sprintf('}%s%s', $name, $description) - : sprintf('}>%s%s', $name, $description); + : sprintf('}%s>%s%s', $listClose, $name, $description); return $strings; } + + /** + * Unwraps lists of lists (`@type array ...$0 {` nested in `@type array ...$0 {`), + * counting the unwrapped lists in `$depth`. + * + * @return list<\PhpStubs\WordPress\Core\WordPressArg> + */ + private static function getListElementChildren(WithChildren $node, int &$depth): array + { + $children = $node->children; + + if (count($children) !== 1 || $children[0]->name !== null || ! $children[0]->hasChildren() || $children[0]->isArrayShape() || $children[0]->isMixedShape()) { + return $children; + } + + $depth += 1; + + return self::getListElementChildren($children[0], $depth); + } + + /** + * Splits a type into the members of its top-level union, keeping the unions + * nested in generics, shapes and callables together. + * + * @return list|null + */ + public static function getUnionMembers(string $type): ?array + { + $member = '(?:[^|<>{}()\'"]++|\'[^\']*\'|"[^"]*"|(?[<{(](?:[^<>{}()\'"]++|\'[^\']*\'|"[^"]*"|(?&bracket))*[>})]))++'; + + if (preg_match_all("#{$member}#", $type, $matches) === false) { + return null; + } + + return array_values( + array_filter( + array_map('trim', $matches[0]), + static function (string $member): bool { + return $member !== ''; + } + ) + ); + } } diff --git a/tests/ParameterTypeTest.php b/tests/ParameterTypeTest.php index 6309311..989d2e6 100644 --- a/tests/ParameterTypeTest.php +++ b/tests/ParameterTypeTest.php @@ -450,4 +450,17 @@ public function testWpUploadBits(): void ] ); } + + public function testWpEnqueueScriptModule(): void + { + $this->analyse( + __DIR__ . '/data/param/wp-enqueue-script-module.php', + [ + ['Parameter #5 $args of function wp_enqueue_script_module expects array{in_footer?: bool, fetchpriority?: \'auto\'|\'high\'|\'low\', ...}, array{in_footer: \'yes\'} given.', 10], + ['Parameter #5 $args of function wp_enqueue_script_module expects array{in_footer?: bool, fetchpriority?: \'auto\'|\'high\'|\'low\', ...}, array{fetchpriority: \'urgent\'} given.', 11], + ['Parameter #5 $args of function wp_enqueue_script_module expects array{in_footer?: bool, fetchpriority?: \'auto\'|\'high\'|\'low\', ...}, array{other: 1} given.', 12], + ['Parameter #5 $args of function wp_enqueue_script_module expects array{in_footer?: bool, fetchpriority?: \'auto\'|\'high\'|\'low\', ...}, array{1: true} given.', 13], + ] + ); + } } diff --git a/tests/VisitorTest.php b/tests/VisitorTest.php index 4cac654..328bf4b 100644 --- a/tests/VisitorTest.php +++ b/tests/VisitorTest.php @@ -188,4 +188,122 @@ function recipient($args = []) {} self::assertStringContainsString('foo?: bool', $listItems); self::assertStringNotContainsString('foo?: int', $listItems); } + + /** + * A shape documented on a generic array becomes an unsealed shape keeping the generic types, + * and only the top-level `string` is removed from the type. + */ + public function testArrayShapeOfGenericArrayIsUnsealed(): void + { + $code = <<<'PHP' + $args { + * Optional. Arguments. + * + * @type bool $in_footer Whether to print in the footer. + * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. + * } + */ + function wpstubs_test_generic_shape($args = []) {} + PHP; + + $stubs = self::generateStubs($code); + + self::assertStringContainsString( + "@phpstan-param array{\n * in_footer?: bool,\n * fetchpriority?: 'auto'|'low'|'high',\n * ...,\n * } \$args", + $stubs + ); + } + + /** + * A list of lists of array shapes is formatted as nested generic arrays. + */ + public function testListOfListsOfArrayShapes(): void + { + $code = <<<'PHP' + > \$fonts", + $stubs + ); + } + + /** + * A parameter type from the function map replaces the one generated from the hash notation. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testMapParamReplacesGeneratedParam(): void + { + $code = <<<'PHP' + 'yes']); // incorrect +wp_enqueue_script_module('id', '', [], false, ['fetchpriority' => 'urgent']); // incorrect +wp_enqueue_script_module('id', '', [], false, ['other' => 1]); // incorrect +wp_enqueue_script_module('id', '', [], false, [1 => true]); // incorrect + +// Correct usage +wp_enqueue_script_module('id'); +wp_enqueue_script_module('id', '', [], false, []); +wp_enqueue_script_module('id', '', [], false, ['in_footer' => true, 'fetchpriority' => 'low']); +wp_enqueue_script_module('id', '', [], false, ['other' => 'value', 'flag' => false]); diff --git a/tests/data/return/wp-dependencies.php b/tests/data/return/wp-dependencies.php index 9213847..14c06a4 100644 --- a/tests/data/return/wp-dependencies.php +++ b/tests/data/return/wp-dependencies.php @@ -32,3 +32,6 @@ assertType('bool', Faker::wpScripts()->query(Faker::nonEmptyString(), 'enqueued')); assertType('_WP_Dependency|false', Faker::wpStyles()->query(Faker::nonEmptyString(), 'registered')); assertType('bool', Faker::wpStyles()->query(Faker::nonEmptyString(), 'enqueued')); + +// Always false if $status is not a known status +assertType('false', Faker::wpDependencies()->query(Faker::nonEmptyString(), 'unknown')); diff --git a/tests/data/return/wp-image-editor.php b/tests/data/return/wp-image-editor.php new file mode 100644 index 0000000..f6d3b03 --- /dev/null +++ b/tests/data/return/wp-image-editor.php @@ -0,0 +1,18 @@ +get_output_format()); + assertType('array{string|null, string|false, string}', $this->get_output_format('image.jpg', 'image/webp')); + } +} From ce912e0ac79a50f9b9e118e8a12b81ea68be7af1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 20 Oct 2026 13:08:41 +0200 Subject: [PATCH 5/5] Regenerate stubs Claude-Session: https://claude.ai/code/session_01WNSb29tptiTQoc44Tk7XZW --- wordpress-stubs.php | 98 +++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 52 deletions(-) diff --git a/wordpress-stubs.php b/wordpress-stubs.php index c924d65..393afed 100644 --- a/wordpress-stubs.php +++ b/wordpress-stubs.php @@ -42457,7 +42457,7 @@ protected function recurse_deps($queue, $handle) * @param string $handle Name of the item. Should be unique. * @param string $status Optional. Status of the item to query. Default 'registered'. * @return bool|_WP_Dependency Found, or object Item data. - * @phpstan-template TStatus 'registered'|'scripts'|'enqueued'|'queued'|'to_do'|'to_print'|'done'|'printed' + * @phpstan-template TStatus of 'registered'|'scripts'|'enqueued'|'queued'|'to_do'|'to_print'|'done'|'printed' * @phpstan-return ($handle is not non-empty-string ? false : ($status is not TStatus ? false : ($status is 'registered'|'scripts' ? _WP_Dependency|false : bool))) */ public function query($handle, $status = 'registered') @@ -45783,6 +45783,7 @@ protected function get_default_quality($mime_type) * @param string $filename * @param string $mime_type * @return array { filename|null, extension, mime-type } + * @phpstan-return array{string|null, string|false, string} */ protected function get_output_format($filename = \null, $mime_type = \null) { @@ -53234,9 +53235,10 @@ class WP_Script_Modules * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional. * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional. * } - * @phpstan-param array{ + * @phpstan-param array{ * in_footer?: bool, * fetchpriority?: 'auto'|'low'|'high', + * ..., * } $args * @phpstan-return void */ @@ -53318,9 +53320,10 @@ public function set_in_footer(string $id, bool $in_footer): bool * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional. * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional. * } - * @phpstan-param array{ + * @phpstan-param array{ * in_footer?: bool, * fetchpriority?: 'auto'|'low'|'high', + * ..., * } $args * @phpstan-return void */ @@ -68844,24 +68847,22 @@ class WP_Font_Face * @param array[][] $fonts Optional. The font-families and their font variations. * See {@see wp_print_font_faces()} for the supported fields. * Default empty array. - * @phpstan-param array[], - * }> $fonts See wp_print_font_faces() + * @phpstan-param array> $fonts See wp_print_font_faces() * @phpstan-return void */ public function generate_and_print(array $fonts) @@ -107651,10 +107652,6 @@ function add_settings_section($id, $title, $callback, $page, $args = array()) * @type string $class CSS Class to be added to the `` element when the * field is output. * } - * @phpstan-param array{ - * label_for?: string, - * class?: string, - * } $args * @phpstan-param array{label_for?: string, class?: string, ...} $args * @phpstan-return void */ @@ -120904,9 +120901,6 @@ function has_term($term = '', $taxonomy = '', $post = \null) * @type string $taxonomy Taxonomy to retrieve terms for. Default 'category'. * } * @return array List of category objects. - * @phpstan-param array{ - * taxonomy?: string, - * } $args * @phpstan-param array{taxonomy?:string,...} $args * @phpstan-return ($args is array{fields: 'count', ...} ? list : ($args is array{fields: 'names'|'slugs', ...} ? list : ($args is array{fields: 'id=>name'|'id=>slug', ...} ? array : ($args is array{fields: 'id=>parent', ...} ? array : ($args is array{fields: 'ids'|'tt_ids', ...} ? list : array))))) */ @@ -129347,24 +129341,22 @@ function fetch_feed($url) * } * } * } - * @phpstan-param array[], - * }> $fonts + * @phpstan-param array> $fonts * @phpstan-return void */ function wp_print_font_faces($fonts = array()) @@ -130540,8 +130532,6 @@ function stripslashes_deep($value) * @phpstan-template T * @phpstan-param T $value * @phpstan-return (T is string ? string : T) - * @phpstan-template T - * @phpstan-param T|string $value * @phpstan-pure */ function stripslashes_from_strings_only($value) @@ -159433,9 +159423,10 @@ function wp_enqueue_block_support_styles($style, $priority = 10) * @type bool $prettify Whether to add new lines and indents to output. * Default to whether the `SCRIPT_DEBUG` constant is defined. * } - * @phpstan-param array{ + * @phpstan-param array{ * optimize?: bool, * prettify?: bool, + * ..., * } $options * @phpstan-return void */ @@ -159462,13 +159453,14 @@ function wp_enqueue_stored_styles($options = array()) * @type string $media The media for which this stylesheet has been defined. * @type string|null $path Absolute path to the stylesheet, so that it can potentially be inlined. * } - * @phpstan-param array|array{ + * @phpstan-param array{ * handle?: string, * src?: string|false, * deps?: string[], * ver?: string|bool|null, * media?: string, * path?: string|null, + * ..., * } $args */ function wp_enqueue_block_style($block_name, $args) @@ -159671,9 +159663,10 @@ function wp_script_modules(): \WP_Script_Modules * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional. * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional. * } - * @phpstan-param array{ + * @phpstan-param array{ * in_footer?: bool, * fetchpriority?: 'auto'|'low'|'high', + * ..., * } $args */ function wp_register_script_module(string $id, string $src, array $deps = array(), $version = \false, array $args = array()) @@ -159718,9 +159711,10 @@ function wp_register_script_module(string $id, string $src, array $deps = array( * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional. * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional. * } - * @phpstan-param array{ + * @phpstan-param array{ * in_footer?: bool, * fetchpriority?: 'auto'|'low'|'high', + * ..., * } $args */ function wp_enqueue_script_module(string $id, string $src = '', array $deps = array(), $version = \false, array $args = array())