From 2230ad7809fe98eaf1830c6ae4867b0f0a704a06 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 09:15:53 +0000 Subject: [PATCH 1/4] Describe the shape of post type, taxonomy and capability objects `WP_Post_Type::$labels`, `WP_Post_Type::$cap`, `WP_Taxonomy::$labels` and `WP_Taxonomy::$cap` are declared as plain `stdClass`, so every label and capability read off them is `mixed`. The three capability maps are `bool[]`, which leaves their keys as `array-key`. The label and capability objects are built by `get_post_type_labels()`, `get_post_type_capabilities()`, `get_taxonomy_labels()` and `WP_Taxonomy::set_props()`, all of which fill in every default, so their contents can be described exactly: - Labels whose default is `null` for one of the two hierarchies, such as `popular_items` on a hierarchical taxonomy, are `string|null`. - `menu_name` and `name_admin_bar` are always present, even though `get_taxonomy_labels()` does not list them. Core reads `menu_name` off a taxonomy in `wp-admin/menu.php`. - The six capabilities that `get_post_type_capabilities()` only adds under `map_meta_cap`, and the conditional `template_name` post type label, are listed as required rather than optional: `map_meta_cap` ends up true for the default capability type, and an optional key would turn every direct read into an "undefined property" error. Note that these object shapes are sealed, so a custom label or capability registered on top of the defaults now needs its own annotation. That already applies to `get_post_type_capabilities()` and `get_taxonomy_labels()`, whose returns are described the same way. The four property entries can be dropped once WordPress ships the matching docblocks and the stubs are regenerated from that release, in the same way `WP_Theme::get()` was removed from the map. Refs https://github.com/szepeviktor/phpstan-wordpress/issues/274 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0122HqysjDiq2jSHebbSSu6A --- functionMap.php | 12 ++++++++++++ phpstan.neon.dist | 2 +- tests/Faker.php | 3 +++ tests/data/return/Faker.php | 3 +++ tests/data/return/wp-capabilities.php | 12 ++++++++++++ tests/data/return/wp-post-type.php | 25 +++++++++++++++++++++++++ tests/data/return/wp-taxonomy.php | 24 ++++++++++++++++++++++++ wordpress-stubs.php | 7 +++++++ 8 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/data/return/wp-capabilities.php create mode 100644 tests/data/return/wp-post-type.php create mode 100644 tests/data/return/wp-taxonomy.php diff --git a/functionMap.php b/functionMap.php index 67ce0e2a..cbf03116 100644 --- a/functionMap.php +++ b/functionMap.php @@ -11,6 +11,11 @@ $wpWidgetRssFormArgsType = 'array{number: int, error: bool, title?: string, url?: string, items?: int, show_summary?: int, show_author?: int, show_date?: int}'; $wpWidgetRssFormInputsType = 'array{title?: bool, url?: bool, items?: bool, show_summary?: bool, show_author?: bool, show_date?: bool}'; $filesystemDirlistReturnType = "false|array"; +$postTypeLabelsType = 'object{name: string, singular_name: string, add_new: string, add_new_item: string, edit_item: string, new_item: string, view_item: string, view_items: string, search_items: string, not_found: string, not_found_in_trash: string, parent_item_colon: string|null, all_items: string, archives: string, attributes: string, insert_into_item: string, uploaded_to_this_item: string, featured_image: string, set_featured_image: string, remove_featured_image: string, use_featured_image: string, menu_name: string, name_admin_bar: string, filter_items_list: string, filter_by_date: string, items_list_navigation: string, items_list: string, item_published: string, item_published_privately: string, item_reverted_to_draft: string, item_trashed: string, item_scheduled: string, item_updated: string, item_link: string, item_link_description: string, template_name: string}'; +$postTypeCapType = 'object{edit_post: string, read_post: string, delete_post: string, edit_posts: string, edit_others_posts: string, delete_posts: string, publish_posts: string, read_private_posts: string, create_posts: string, read: string, delete_private_posts: string, delete_published_posts: string, delete_others_posts: string, edit_private_posts: string, edit_published_posts: string}'; +$taxonomyLabelsType = 'object{name: string, singular_name: string, menu_name: string, name_admin_bar: string, search_items: string, popular_items: string|null, all_items: string, parent_item: string|null, parent_item_colon: string|null, name_field_description: string, slug_field_description: string, parent_field_description: string|null, desc_field_description: string, edit_item: string, view_item: string, update_item: string, add_new_item: string, new_item_name: string, template_name: string, separate_items_with_commas: string|null, add_or_remove_items: string|null, choose_from_most_used: string|null, not_found: string, no_terms: string, filter_by_item: string|null, items_list_navigation: string, items_list: string, most_used: string, back_to_items: string, item_link: string, item_link_description: string}'; +$taxonomyCapType = 'object{manage_terms: string, edit_terms: string, delete_terms: string, assign_terms: string}'; +$capabilityMapType = 'array'; /** * This array follows a format similar to PHPStan’s function map: @@ -381,6 +386,8 @@ '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'"], + 'WP_Post_Type::$cap' => [null, '@phpstan-var' => $postTypeCapType], + 'WP_Post_Type::$labels' => [null, '@phpstan-var' => $postTypeLabelsType], 'WP_Query' => [null, '@phpstan-property-read bool $query_vars_changed' => '', '@phpstan-property-read bool|string $query_vars_hash' => '', '@phpstan-method void init_query_flags()' => ''], 'WP_Query::have_posts' => [null, '@phpstan-impure' => ''], 'WP_REST_Request' => [null, '@phpstan-template T' => 'of array = array', '@phpstan-implements ArrayAccess, value-of>' => ''], @@ -392,6 +399,9 @@ 'WP_REST_Request::offsetSet' => ['void', '@phpstan-template TOffset' => 'of key-of', 'offset' => 'TOffset', 'value' => 'T[TOffset]'], 'WP_REST_Request::offsetUnset' => ['void', '@phpstan-template TOffset' => 'of key-of', 'offset' => 'TOffset'], 'WP_REST_Request::set_param' => ['void', '@phpstan-template TOffset' => 'of key-of', 'key' => 'TOffset', 'value' => 'T[TOffset]'], + 'WP_Role::$capabilities' => [null, '@phpstan-var' => $capabilityMapType], + 'WP_Taxonomy::$cap' => [null, '@phpstan-var' => $taxonomyCapType], + 'WP_Taxonomy::$labels' => [null, '@phpstan-var' => $taxonomyLabelsType], 'WP_Theme' => [ null, '@phpstan-type ThemeKey' => "'Name'|'Version'|'Status'|'Title'|'Author'|'Author Name'|'Author URI'|'Description'|'Template'|'Stylesheet'|'Template Files'|'Stylesheet Files'|'Template Dir'|'Stylesheet Dir'|'Screenshot'|'Tags'|'Theme Root'|'Theme Root URI'|'Parent Theme'", @@ -415,6 +425,8 @@ 'WP_Theme_JSON_Resolver::get_theme_data' => [null, 'deprecated' => 'array{}'], 'WP_Translations::translate' => ['($singular is null ? null : string)'], 'WP_Translations::translate_plural' => ['($singular is null ? null : ($plural is null ? T : string))', '@phpstan-template T' => 'of string|null', 'singular' => 'T', 'count' => 'int'], + 'WP_User::$allcaps' => [null, '@phpstan-var' => $capabilityMapType], + 'WP_User::$caps' => [null, '@phpstan-var' => $capabilityMapType], 'WP_Widget' => [null, '@phpstan-template T' => 'of array = array'], 'WP_Widget::display_callback' => [null, '@final' => ''], 'WP_Widget::form' => [null, 'instance' => 'T'], diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 249096be..97ce797b 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -15,7 +15,7 @@ parameters: - path: tests/Faker.php identifier: class.notFound - count: 20 + count: 23 - # PHPStan\Analyser\Analyser::analyse() # PHPStan\Analyser\AnalyserResult::getErrors() diff --git a/tests/Faker.php b/tests/Faker.php index e6127546..a9b84771 100644 --- a/tests/Faker.php +++ b/tests/Faker.php @@ -35,12 +35,15 @@ * @method static \WP_Locale wpLocale() * @method static \WP_Object_Cache wpObjectCache() * @method static \WP_Post wpPost() + * @method static \WP_Post_Type wpPostType() * @method static \WP_Query wpQuery() * @method static \WP_REST_Request wpRestRequest() * @method static \WP_REST_Response wpRestResponse() + * @method static \WP_Role wpRole() * @method static \WP_Screen wpScreen() * @method static \WP_Scripts wpScripts() * @method static \WP_Styles wpStyles() + * @method static \WP_Taxonomy wpTaxonomy() * @method static \WP_Term wpTerm() * @method static \WP_Theme wpTheme() * @method static \WP_Translations wpTranslations() diff --git a/tests/data/return/Faker.php b/tests/data/return/Faker.php index 3a63c579..b7a7d8cf 100644 --- a/tests/data/return/Faker.php +++ b/tests/data/return/Faker.php @@ -79,12 +79,15 @@ assertType('WP_Error', Faker::wpError()); assertType('WP_Object_Cache', Faker::wpObjectCache()); assertType('WP_Post', Faker::wpPost()); +assertType('WP_Post_Type', Faker::wpPostType()); assertType('WP_Query', Faker::wpQuery()); assertType('WP_REST_Request', Faker::wpRestRequest()); assertType('WP_REST_Response', Faker::wpRestResponse()); +assertType('WP_Role', Faker::wpRole()); assertType('WP_Screen', Faker::wpScreen()); assertType('WP_Scripts', Faker::wpScripts()); assertType('WP_Styles', Faker::wpStyles()); +assertType('WP_Taxonomy', Faker::wpTaxonomy()); assertType('WP_Term', Faker::wpTerm()); assertType('WP_Theme', Faker::wpTheme()); assertType('WP_Translations', Faker::wpTranslations()); diff --git a/tests/data/return/wp-capabilities.php b/tests/data/return/wp-capabilities.php new file mode 100644 index 00000000..573a0310 --- /dev/null +++ b/tests/data/return/wp-capabilities.php @@ -0,0 +1,12 @@ +', Faker::wpUser()->caps); +assertType('array', Faker::wpUser()->allcaps); +assertType('array', Faker::wpRole()->capabilities); diff --git a/tests/data/return/wp-post-type.php b/tests/data/return/wp-post-type.php new file mode 100644 index 00000000..bee1d039 --- /dev/null +++ b/tests/data/return/wp-post-type.php @@ -0,0 +1,25 @@ +labels->name); +assertType('string', Faker::wpPostType()->labels->singular_name); +assertType('string', Faker::wpPostType()->labels->menu_name); +assertType('string', Faker::wpPostType()->labels->name_admin_bar); +assertType('string', Faker::wpPostType()->labels->item_link_description); + +// Only used on hierarchical post types, where the default is null otherwise. +assertType('string|null', Faker::wpPostType()->labels->parent_item_colon); + +// Capabilities are built by get_post_type_capabilities(). +assertType('string', Faker::wpPostType()->cap->edit_post); +assertType('string', Faker::wpPostType()->cap->edit_posts); +assertType('string', Faker::wpPostType()->cap->create_posts); + +// Only present when the post type is registered with 'map_meta_cap'. +assertType('string', Faker::wpPostType()->cap->edit_published_posts); diff --git a/tests/data/return/wp-taxonomy.php b/tests/data/return/wp-taxonomy.php new file mode 100644 index 00000000..d414a6a5 --- /dev/null +++ b/tests/data/return/wp-taxonomy.php @@ -0,0 +1,24 @@ +labels->name); +assertType('string', Faker::wpTaxonomy()->labels->singular_name); +assertType('string', Faker::wpTaxonomy()->labels->menu_name); +assertType('string', Faker::wpTaxonomy()->labels->name_admin_bar); +assertType('string', Faker::wpTaxonomy()->labels->template_name); + +// These have no default for one of the two hierarchies. +assertType('string|null', Faker::wpTaxonomy()->labels->popular_items); +assertType('string|null', Faker::wpTaxonomy()->labels->parent_item); +assertType('string|null', Faker::wpTaxonomy()->labels->filter_by_item); + +assertType('string', Faker::wpTaxonomy()->cap->manage_terms); +assertType('string', Faker::wpTaxonomy()->cap->edit_terms); +assertType('string', Faker::wpTaxonomy()->cap->delete_terms); +assertType('string', Faker::wpTaxonomy()->cap->assign_terms); diff --git a/wordpress-stubs.php b/wordpress-stubs.php index 6475c0bd..a72b6ba3 100644 --- a/wordpress-stubs.php +++ b/wordpress-stubs.php @@ -48532,6 +48532,7 @@ final class WP_Post_Type * * @since 4.6.0 * @var stdClass $labels + * @phpstan-var object{name: string, singular_name: string, add_new: string, add_new_item: string, edit_item: string, new_item: string, view_item: string, view_items: string, search_items: string, not_found: string, not_found_in_trash: string, parent_item_colon: string|null, all_items: string, archives: string, attributes: string, insert_into_item: string, uploaded_to_this_item: string, featured_image: string, set_featured_image: string, remove_featured_image: string, use_featured_image: string, menu_name: string, name_admin_bar: string, filter_items_list: string, filter_by_date: string, items_list_navigation: string, items_list: string, item_published: string, item_published_privately: string, item_reverted_to_draft: string, item_trashed: string, item_scheduled: string, item_updated: string, item_link: string, item_link_description: string, template_name: string} */ public $labels; /** @@ -48796,6 +48797,7 @@ final class WP_Post_Type * * @since 4.6.0 * @var stdClass $cap + * @phpstan-var object{edit_post: string, read_post: string, delete_post: string, edit_posts: string, edit_others_posts: string, delete_posts: string, publish_posts: string, read_private_posts: string, create_posts: string, read: string, delete_private_posts: string, delete_published_posts: string, delete_others_posts: string, edit_private_posts: string, edit_published_posts: string} */ public $cap; /** @@ -52149,6 +52151,7 @@ class WP_Role * @since 2.0.0 * @var bool[] Array of key/value pairs where keys represent a capability name and boolean values * represent whether the role has that capability. + * @phpstan-var array */ public $capabilities; /** @@ -54556,6 +54559,7 @@ final class WP_Taxonomy * * @since 4.7.0 * @var stdClass + * @phpstan-var object{name: string, singular_name: string, menu_name: string, name_admin_bar: string, search_items: string, popular_items: string|null, all_items: string, parent_item: string|null, parent_item_colon: string|null, name_field_description: string, slug_field_description: string, parent_field_description: string|null, desc_field_description: string, edit_item: string, view_item: string, update_item: string, add_new_item: string, new_item_name: string, template_name: string, separate_items_with_commas: string|null, add_or_remove_items: string|null, choose_from_most_used: string|null, not_found: string, no_terms: string, filter_by_item: string|null, items_list_navigation: string, items_list: string, most_used: string, back_to_items: string, item_link: string, item_link_description: string} */ public $labels; /** @@ -54656,6 +54660,7 @@ final class WP_Taxonomy * * @since 4.7.0 * @var stdClass + * @phpstan-var object{manage_terms: string, edit_terms: string, delete_terms: string, assign_terms: string} */ public $cap; /** @@ -59380,6 +59385,7 @@ class WP_User * @since 2.0.0 * @var bool[] Array of key/value pairs where keys represent a capability name * and boolean values represent whether the user has that capability. + * @phpstan-var array */ public $caps = array(); /** @@ -59402,6 +59408,7 @@ class WP_User * @since 2.0.0 * @var bool[] Array of key/value pairs where keys represent a capability name * and boolean values represent whether the user has that capability. + * @phpstan-var array */ public $allcaps = array(); /** From 164974745bccc952c277f146a3900cf0626793e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 10:33:29 +0000 Subject: [PATCH 2/4] Inherit a property's shape from the function it points at WordPress documents the contents of an object like `WP_Post_Type::$labels` on the function that builds it, and refers to that function from the property with `@see`. The visitor already follows that convention for `@param`, through `discoverInheritedArgs()`, but not for `@var`, so a property stayed `stdClass` however well the function it points at was documented. Describing those properties in core therefore meant repeating a thirty-key list on the property, which is exactly what the `@see` exists to avoid. `@var` now follows the same reference. A property inherits the `@phpstan-return` shape of a function it `@see`s when the two types agree, so `object` shapes only land on `object` properties and `array` shapes on `array` ones. A shape of the property's own always wins, whether it comes from hash notation in the source or from the function map. Generating the stubs for WordPress 7.0.1 before and after produces an identical file: the four properties this was written for are still covered by their function map entries, which take precedence, and nothing else in core both carries a matching `@see` and lacks a shape of its own. Refs https://github.com/szepeviktor/phpstan-wordpress/issues/274 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0122HqysjDiq2jSHebbSSu6A --- functionMap.php | 9 +++ src/Visitor.php | 90 ++++++++++++++++++++++++++++ tests/VisitorTest.php | 134 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+) diff --git a/functionMap.php b/functionMap.php index cbf03116..e2060be9 100644 --- a/functionMap.php +++ b/functionMap.php @@ -11,6 +11,15 @@ $wpWidgetRssFormArgsType = 'array{number: int, error: bool, title?: string, url?: string, items?: int, show_summary?: int, show_author?: int, show_date?: int}'; $wpWidgetRssFormInputsType = 'array{title?: bool, url?: bool, items?: bool, show_summary?: bool, show_author?: bool, show_date?: bool}'; $filesystemDirlistReturnType = "false|array"; +// WordPress declares the objects below as plain `stdClass` and the capability maps as +// `bool[]`, so their contents are `mixed` and their keys `array-key`. The shapes are taken +// from get_post_type_labels(), get_post_type_capabilities(), get_taxonomy_labels() and +// WP_Taxonomy::set_props(), which fill in every default. +// +// These entries are temporary. https://github.com/WordPress/wordpress-develop/pull/13220 +// documents the same shapes in core, on the functions that build them and via `@see` from +// the properties, and the visitor picks those up on its own. Drop these once the stubs are +// generated from a release carrying that change. $postTypeLabelsType = 'object{name: string, singular_name: string, add_new: string, add_new_item: string, edit_item: string, new_item: string, view_item: string, view_items: string, search_items: string, not_found: string, not_found_in_trash: string, parent_item_colon: string|null, all_items: string, archives: string, attributes: string, insert_into_item: string, uploaded_to_this_item: string, featured_image: string, set_featured_image: string, remove_featured_image: string, use_featured_image: string, menu_name: string, name_admin_bar: string, filter_items_list: string, filter_by_date: string, items_list_navigation: string, items_list: string, item_published: string, item_published_privately: string, item_reverted_to_draft: string, item_trashed: string, item_scheduled: string, item_updated: string, item_link: string, item_link_description: string, template_name: string}'; $postTypeCapType = 'object{edit_post: string, read_post: string, delete_post: string, edit_posts: string, edit_others_posts: string, delete_posts: string, publish_posts: string, read_private_posts: string, create_posts: string, read: string, delete_private_posts: string, delete_published_posts: string, delete_others_posts: string, edit_private_posts: string, edit_published_posts: string}'; $taxonomyLabelsType = 'object{name: string, singular_name: string, menu_name: string, name_admin_bar: string, search_items: string, popular_items: string|null, all_items: string, parent_item: string|null, parent_item_colon: string|null, name_field_description: string, slug_field_description: string, parent_field_description: string|null, desc_field_description: string, edit_item: string, view_item: string, update_item: string, add_new_item: string, new_item_name: string, template_name: string, separate_items_with_commas: string|null, add_or_remove_items: string|null, choose_from_most_used: string|null, not_found: string, no_terms: string, filter_by_item: string|null, items_list_navigation: string, items_list: string, most_used: string, back_to_items: string, item_link: string, item_link_description: string}'; diff --git a/src/Visitor.php b/src/Visitor.php index c7270573..327ee1d8 100644 --- a/src/Visitor.php +++ b/src/Visitor.php @@ -283,6 +283,7 @@ private function addTags(string $name, Doc $docComment): ?Doc } $additions = $this->discoverInheritedArgs($docblock, $additions); + $additions = $this->discoverInheritedVar($name, $docblock, $docCommentText, $additions); /** @var list $additionStrings */ $additionStrings = array_map( @@ -357,6 +358,95 @@ static function (WordPressTag $addition): bool { return $additions; } + /** + * Gives a property the shape of the value returned by the function it points at. + * + * WordPress documents the contents of an object such as `WP_Post_Type::$labels` on the + * function that builds it, and refers to that function from the property with `@see`, + * so the shape is only written out once. + * + * A shape of the property's own always wins, whether it comes from hash notation in the + * source or from the function map. + * + * @param list<\PhpStubs\WordPress\Core\WordPressTag> $additions + * @return list<\PhpStubs\WordPress\Core\WordPressTag> + */ + private function discoverInheritedVar(string $symbolName, DocBlock $docblock, string $docCommentText, array $additions): array + { + if (! str_contains($symbolName, '::$')) { + return $additions; + } + + foreach ($additions as $addition) { + if ($addition->tag === '@phpstan-var') { + return $additions; + } + } + + foreach ($this->additionalTagStrings[$symbolName] ?? [] as $mappedTag) { + if (str_starts_with($mappedTag, '@phpstan-var ')) { + return $additions; + } + } + + $propertyType = self::getVarTypeName($docblock); + + if ($propertyType === null) { + return $additions; + } + + $inherited = $this->getInheritedTagForVar($docCommentText, $propertyType); + + if (! ($inherited instanceof WordPressTag)) { + return $additions; + } + + return array_merge($additions, [$inherited]); + } + + private static function getVarTypeName(DocBlock $docblock): ?string + { + /** @var list<\phpDocumentor\Reflection\DocBlock\Tags\Var_> $varTags */ + $varTags = $docblock->getTagsByName('var'); + + foreach ($varTags as $varTag) { + $type = $varTag->getType(); + + if (! $type instanceof Type) { + continue; + } + + return self::getTypeNameFromType($type); + } + + return null; + } + + private function getInheritedTagForVar(string $docCommentText, string $propertyType): ?WordPressTag + { + foreach ($this->additionalTags as $symbolName => $tags) { + if (! str_contains($docCommentText, sprintf('@see %s()', $symbolName))) { + continue; + } + + foreach ($tags as $tag) { + // Only a documented shape of a matching type can describe the property. + if ($tag->tag !== '@phpstan-return' || $tag->type !== $propertyType || ! $tag->hasChildren()) { + continue; + } + + $inherited = clone $tag; + $inherited->tag = '@phpstan-var'; + $inherited->name = null; + $inherited->description = sprintf('See %s()', $symbolName); + + return $inherited; + } + } + + return null; + } + /** * @return list<\PhpStubs\WordPress\Core\WordPressTag> */ diff --git a/tests/VisitorTest.php b/tests/VisitorTest.php index 93f8908c..1d15bc95 100644 --- a/tests/VisitorTest.php +++ b/tests/VisitorTest.php @@ -39,6 +39,38 @@ private static function generateStubs(string $code): string return (new Result($visitor, []))->prettyPrint(); } + /** + * The `@phpstan-var` tag of the stubs, up to the end of its shape. + */ + private static function phpStanVarTag(string $stubs): string + { + $position = strpos($stubs, '@phpstan-var'); + self::assertIsInt($position, $stubs); + + $tag = strstr(substr($stubs, $position), '}', true); + self::assertIsString($tag, $stubs); + + return $tag; + } + + /** + * A function documenting the shape of the object it returns, for the property tests below. + */ + private const REFERENCED_FUNCTION = <<<'PHP' + Date: Fri, 21 Aug 2026 12:38:16 +0200 Subject: [PATCH 3/4] Update PHPCS config --- phpcs.xml.dist | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a6a48724..1363bfc5 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -12,6 +12,14 @@ + + + + + + + + tests/TypeInferenceTest.php From 69f7cefa70c0cdbf482b19c7a62410015bb4a55c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 21 Aug 2026 12:38:31 +0200 Subject: [PATCH 4/4] Update Composer config --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0128032d..9a3de41e 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,8 @@ "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", "symfony/polyfill-php80": "*", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", - "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.2", + "wp-coding-standards/wpcs": "3.4.1 as 2.3.0" }, "conflict": { "phpdocumentor/reflection-docblock": "5.6.1"