Skip to content

Code Quality: Define array shapes for block type selectors and viewport media queries - #15644

Closed
westonruter wants to merge 8 commits into
WordPress:trunkfrom
westonruter:fix/block-type-selectors-shape
Closed

westonruter wants to merge 8 commits into
WordPress:trunkfrom
westonruter:fix/block-type-selectors-shape

Conversation

@westonruter

@westonruter westonruter commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

✅ Committed in r65318 (28d2f50).


This defines array shapes for block type selectors and for viewport media queries, which Gutenberg PRs depend on (see below).

Block type selectors

WP_Block_Type::$selectors is documented as a plain array. This defines its shape, following the selectors schema in block.json: an optional root selector string, plus selectors for block support features (such as color or typography), each either a selector string or an array of selectors keyed by subfeature (such as root or text).

// Plain @var, @param, and @return tags:
array<string, string|array<string, string>>

// @phpstan- tags:
array{ root?: string, ...<string, string|array<string, string>> }

The shape is unsealed, so any feature key is allowed. Core itself uses keys the schema doesn't list (states and filter), and third-party blocks can add their own. The plain form matches what WP_Theme_JSON::get_feature_selector() already uses.

The shape is applied to:

  • The WP_Block_Type::$selectors property, whose description now also covers the states key, along with the selectors argument to the constructor.
  • The return value of WP_Theme_JSON::get_block_selectors(), whose $block_type parameter is also now documented as WP_Block_Type rather than object.
  • The selectors key of the $metadata parameter of WP_Theme_JSON::get_feature_declarations_for_node().
  • The $block_selectors parameter of wp_get_state_style_groups().

There is one code change. WP_Theme_JSON::get_block_selectors() now builds the feature selectors first and prepends root with array( 'root' => $root_selector ) + $feature_selectors. The result has the same keys in the same order, but since BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS is accessed via static::, a subclass's feature named root can no longer replace the root selector string with an array.

New tests in Tests_Theme_wpThemeJson cover get_block_selectors():

  • test_get_block_selectors_builds_default_selectors_with_root_first(): For a block without custom selectors, the default selectors list root first, followed by the feature selectors in the order of BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS.
  • test_get_block_selectors_returns_custom_selectors(): Custom selectors set by the block are returned as they are.
  • test_get_block_selectors_keeps_root_selector_string(): For a subclass that maps a support to a feature named root, the root selector string is kept.

Building the default selectors with $feature_selectors['root'] ??= $root_selector instead fails the first and third tests.

The $state_style parameter of wp_get_state_style_groups() is also now documented as array<string, mixed>, since it is keyed by style feature and each key is looked up in $block_selectors.

At rule level 10, these changes resolve 10 PHPStan errors, including missingType.iterableValue on the property and on the two functions, return.type for mixed returned from wp_get_block_css_selector() and WP_Theme_JSON::get_block_selectors(), and argument.type for mixed passed to wp_add_state_style_group(). They introduce one: the caller wp_get_block_state_style_rules() narrows each state style from mixed only with is_array(), so it now reports an argument.type error for passing array<mixed, mixed> as $state_style. That is a more accurate account of the untyped theme.json data than the missingType.iterableValue error it replaces. None of these errors are in the level 5 baselines.

Viewport media queries

WP_Theme_JSON::get_viewport_media_queries() only ever returns the @mobile, @tablet, and @desktop keys, each holding a non-empty @media query string, so this documents that in a @phpstan-return array shape:

array{
    '@mobile'?: non-falsy-string,
    '@tablet'?: non-falsy-string,
    '@desktop'?: non-falsy-string,
}

Without it, callers see each media query as mixed. The Navigation block passes them to wp_style_engine_get_stylesheet_from_css_rules() as the rules_group, which expects a string.

Gutenberg PRs depending on this

These Gutenberg PRs generate their WordPress stubs for PHPStan from this PR, so that the analysis sees these types:

Once this is committed, they will need their stubs regenerated from trunk before they are merged.

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

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5 (medium effort)
Used for: Deriving the array shape from the block.json schema and the code that reads the selectors, applying it, comparing PHPStan results before and after, running the related PHPUnit tests, and writing this description. Directed and reviewed by me.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

`WP_Block_Type::$selectors` was documented as a plain `array`. Its shape follows the `selectors` schema in `block.json`: an optional `root` selector string, plus selectors for block support features (such as `color` or `typography`), each either a selector string or an array of selectors keyed by subfeature (such as `root` or `text`). Any feature key is allowed, since third-party blocks and the `states` and `filter` keys are not covered by the schema's listed properties.

Document this as `array<string, string|array<string, string>>`, with the more specific `array{ root?: string, ...<string, string|array<string, string>> }` in `@phpstan-` tags, on the property, on the `selectors` argument to the constructor, on the return value of `WP_Theme_JSON::get_block_selectors()`, on the `$block_selectors` parameter of `wp_get_state_style_groups()`, and on the `selectors` key of the `$metadata` parameter of `WP_Theme_JSON::get_feature_declarations_for_node()`. Also document the `$block_type` parameter of `WP_Theme_JSON::get_block_selectors()` as `WP_Block_Type` rather than `object`, since it comes from the block type registry and is passed to `wp_get_block_css_selector()`.

In `WP_Theme_JSON::get_block_selectors()`, the default selectors are now built by prepending the `root` selector to the feature selectors, rather than adding the feature selectors after it. The keys and their order are unchanged, but a feature named `root` in a subclass's `BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS` can no longer replace the root selector string with an array.

This resolves 9 PHPStan errors, including `missingType.iterableValue` on the property and the two functions, `return.type` for `mixed` returned from `wp_get_block_css_selector()` and `WP_Theme_JSON::get_block_selectors()`, and `argument.type` for `mixed` passed to `wp_add_state_style_group()`.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 7a12dabb-d901-4983-86e2-a9cbfedc1158
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

westonruter and others added 3 commits November 6, 2026 00:54
…()`.

The `$state_style` parameter is a theme.json style object, keyed by style feature (such as `color` or `typography`), and each key is looked up in `$block_selectors`. Its values come from theme.json and are checked by the function itself, so they stay `mixed`.

This resolves the `missingType.iterableValue` PHPStan error on the parameter. The caller, `wp_get_block_state_style_rules()`, narrows each state style from `mixed` only with `is_array()`, so it now reports an `argument.type` error for passing `array<mixed, mixed>`, which is a more accurate account of the untyped theme.json data.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 7a12dabb-d901-4983-86e2-a9cbfedc1158
`WP_Theme_JSON::get_viewport_media_queries()` only ever returns the `@mobile`, `@tablet`, and `@desktop` keys, each holding a non-empty `@media` query string, so document that in a `@phpstan-return` array shape.

Without it, callers see each media query as `mixed`. The Navigation block passes them to `wp_style_engine_get_stylesheet_from_css_rules()` as the `rules_group`, which expects a string.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 32212707-a7f1-4bca-bef2-7f3b4982950b
westonruter added a commit to huzaifaalmesbah/gutenberg that referenced this pull request Nov 6, 2026
This reverts commit aed1887.

The guard was only needed because Core's `WP_Theme_JSON::get_viewport_media_queries()` documented a plain `array` return. That method now has an array shape in WordPress/wordpress-develop#15644, so with stubs generated from it, each media query is already a `non-falsy-string` and `rules_group` is narrowed without the check.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 32212707-a7f1-4bca-bef2-7f3b4982950b
westonruter added a commit to huzaifaalmesbah/gutenberg that referenced this pull request Nov 6, 2026
…p#15644

That pull request adds the array shape for `WP_Theme_JSON::get_viewport_media_queries()`, which the Navigation block falls back to, so that its media queries are known to be strings when passed to the style engine as a `rules_group`.

Regenerate the stubs with `--commit=trunk` once it is committed to Core, before this is merged.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 32212707-a7f1-4bca-bef2-7f3b4982950b
@westonruter westonruter changed the title Code Quality: Define the array shape for block type selectors Code Quality: Define array shapes for block type selectors and viewport media queries Nov 6, 2026
westonruter and others added 2 commits November 6, 2026 11:44
The `WP_Block_Type::$selectors` description said that every key other than `root` is a block support feature, but the `states` key instead holds the selectors for the block's custom states, keyed by state name (such as `-current`).

Also realign the `@type` rows of the `$args` hash in the constructor's docblock, which the longer type of the `selectors` argument had pushed out of line.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 7a12dabb-d901-4983-86e2-a9cbfedc1158
…ctors()`.

Add tests that:

* The default selectors for a block without custom selectors list the root selector first, followed by the feature selectors in the order of `BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS`.
* Custom selectors set by the block are returned as they are.
* A feature named `root`, in a subclass that overrides `BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS`, does not replace the root selector string.

Building the default selectors with `$feature_selectors['root'] ??= $root_selector` instead fails the first and third tests.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 7a12dabb-d901-4983-86e2-a9cbfedc1158
@westonruter
westonruter requested a review from ramonjd November 6, 2026 18:58
@westonruter
westonruter marked this pull request as ready for review November 6, 2026 18:58
@westonruter
westonruter requested a lite review from Copilot November 6, 2026 18:58
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props westonruter.

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


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active issues were identified, and the change is narrowly scoped to typed selector metadata, root-selector preservation, and focused test coverage.

No open issues 🎉

What changed in this PR

This pull request adds precise PHPDoc/PHPStan array shapes for block selectors and viewport media queries, while preserving the root selector during default selector generation. It also adds focused PHPUnit coverage.

Changes:

  • Documents selector shapes across block types, theme JSON processing, and state-style helpers.
  • Documents optional @mobile, @tablet, and @desktop media-query keys as non-empty strings.
  • Refactors default selector construction to preserve the root selector.
  • Adds tests for selector ordering, custom selectors, and subclass root-selector handling.
File Description Review note
tests/​phpunit/​tests/​theme/​wpThemeJson.php Tests default selector ordering, custom selector passthrough, and root-selector preservation. —
src/​wp-includes/​class-wp-theme-json.php Adds selector and viewport media-query types and preserves the root selector during default generation. Nit (1 vote): Keep the metadata shape open while narrowing the selectors key, since call sites provide additional keys such as path, selector, elements, and variations.
src/​wp-includes/​class-wp-block-type.php Documents the structure of block selector metadata on the property and constructor arguments. —
src/​wp-includes/​block-supports/​states.php Adds precise types for state styles and block selector metadata. —

Review effort: Lite

westonruter and others added 2 commits November 6, 2026 12:02
… helper.

The helper calls `get_block_selectors()` on `WP_Theme_JSON` or a subclass of it, so document `$class_name` as `class-string<WP_Theme_JSON>` rather than `string`.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 7a12dabb-d901-4983-86e2-a9cbfedc1158
…tadata cache.

The test for a feature named `root` creates an anonymous subclass of `WP_Theme_JSON` to get its class name. Creating it ran the `WP_Theme_JSON` constructor, which calls `static::get_blocks_metadata()` and fills the blocks metadata cache that the subclass shares with `WP_Theme_JSON`. That cached metadata used the subclass's feature level selectors, where `color` maps to `root`, for every registered block not already cached. As a result, later tests in the same process could have seen core blocks without their color feature selectors, depending on test order.

Give the subclass an empty constructor, so that creating it leaves the cache alone.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 7a12dabb-d901-4983-86e2-a9cbfedc1158
wporg-sync pushed a commit that referenced this pull request Nov 6, 2026
The `WP_Block_Type::$selectors` property was documented only as an `array`, so PHPStan saw every selector read from it as `mixed`. Its shape now follows the `selectors` schema in `block.json`: an optional `root` selector string, plus the selectors for each block support feature, each either a string or an array of selectors keyed by subfeature. The shape stays open to other keys, since core blocks also use `states` and `filter`, and third-party blocks can add their own. It is applied wherever the selectors pass through `WP_Theme_JSON` and the block state styles.

Similarly, `WP_Theme_JSON::get_viewport_media_queries()` only ever returns the `@mobile`, `@tablet`, and `@desktop` keys, each holding a media query string, so its return value is now documented with that shape. Callers such as the Navigation block, which passes each query to `wp_style_engine_get_stylesheet_from_css_rules()` as the `rules_group`, no longer see them as `mixed`.

Developed in #15644.
Follow-up to r55673, r56058, r62453, r62671, r64727.

See #65817.


git-svn-id: https://develop.svn.wordpress.org/trunk@65318 602fd350-edb4-49c9-b593-d223f7449a82
@westonruter westonruter closed this Nov 6, 2026
wporg-sync pushed a commit to WordPress/WordPress that referenced this pull request Nov 6, 2026
The `WP_Block_Type::$selectors` property was documented only as an `array`, so PHPStan saw every selector read from it as `mixed`. Its shape now follows the `selectors` schema in `block.json`: an optional `root` selector string, plus the selectors for each block support feature, each either a string or an array of selectors keyed by subfeature. The shape stays open to other keys, since core blocks also use `states` and `filter`, and third-party blocks can add their own. It is applied wherever the selectors pass through `WP_Theme_JSON` and the block state styles.

Similarly, `WP_Theme_JSON::get_viewport_media_queries()` only ever returns the `@mobile`, `@tablet`, and `@desktop` keys, each holding a media query string, so its return value is now documented with that shape. Callers such as the Navigation block, which passes each query to `wp_style_engine_get_stylesheet_from_css_rules()` as the `rules_group`, no longer see them as `mixed`.

Developed in WordPress/wordpress-develop#15644.
Follow-up to r55673, r56058, r62453, r62671, r64727.

See #65817.

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


git-svn-id: http://core.svn.wordpress.org/trunk@64458 1a063a9b-81f0-0310-95a4-ce76da25c4cd
westonruter added a commit to huzaifaalmesbah/gutenberg that referenced this pull request Nov 6, 2026
Resolve the conflicts in the generated files by generating them again. The WordPress stubs are generated from wordpress-develop trunk at r65320, which includes the shapes from WordPress/wordpress-develop#15644, and the PHPStan baseline is regenerated with them.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 32212707-a7f1-4bca-bef2-7f3b4982950b
westonruter added a commit to WordPress/gutenberg that referenced this pull request Nov 6, 2026
* PHPStan: Resolve always-true type checks

- Drop the `method_exists()` check for `WP_Interactivity_API::add_client_navigation_support_to_script_module()` in `lib/client-assets.php`. The method was added in WordPress 6.9, and the plugin requires 7.0.
- Document the Navigation block's `$layout` parameter as `mixed`. It comes from block attributes, so the existing `is_array()` guards are needed.
- Ignore the `method_exists()` check for `WP_Theme_JSON_Gutenberg::get_viewport_media_queries()` in the Navigation block, which is needed when the block library runs in WordPress Core, where that class is not defined.

Remove the 4 resolved entries from the PHPStan baseline.

See #86088.

* Navigation: Narrow layout param instead of widening it to mixed

The only caller of `block_core_navigation_get_layout_custom_property_declarations()` passes the `array_replace()` of two values it has already checked with `is_array()`, so `$layout` is always an array. Document it as `array<string, mixed>` rather than `mixed`, and remove the three redundant `is_array( $layout )` guards. Each offset is still read with `??`, and `justifyContent` is still checked with `is_string()`, so unexpected values fall back to the defaults as before.

The return type is narrowed to `array<string, string>`.

At the call site, the merged layout gets an inline `@var array<string, mixed>`, since PHPStan types an array narrowed from an attribute as `array<mixed>`, while the layout attribute is a JSON object with string keys.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Navigation: Add a return shape for layout custom property declarations

Every key `block_core_navigation_get_layout_custom_property_declarations()` returns is fixed, and each value comes from a small set of literals, so document them in a `@phpstan-return` array shape.

For PHPStan to verify the `--navigation-layout-align` values, the vertical alignment is now looked up in its own map rather than chosen with `in_array()`. PHPStan does not tie the `in_array()` check to the `$justification` it picks, so it inferred that `space-between` could be returned. The behavior is unchanged: `center` and `right` map to `center` and `flex-end`, and everything else falls back to `flex-start`.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Code Quality: Type the script module registry entries

Annotate the `$modules` loaded in `gutenberg_define_interactivity_modules_support()` with the shape that wp-build's `generateModuleRegistrationPhp()` writes for each entry: an `id`, `path`, and `asset`, plus `min_only` set to `true` for modules that only ship minified, such as WASM workers. This resolves the `mixed` offset access errors at higher PHPStan levels.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Navigation: Combine the layout comment with its `@var` docblock

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Code Quality: Add a return shape for viewport media queries

`WP_Theme_JSON_Gutenberg::get_viewport_media_queries()` only ever returns the `@mobile`, `@tablet`, and `@desktop` keys, each holding a non-empty `@media` query string, so document that in a `@phpstan-return` array shape.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Navigation: Skip viewport media queries that are not strings

The media queries come from `WP_Theme_JSON_Gutenberg::get_viewport_media_queries()` or, when that is unavailable, from Core's `WP_Theme_JSON::get_viewport_media_queries()`. Core's method only documents a plain `array` return, so the union of the two leaves each `$media_query` as `mixed`. That value is passed as the `rules_group` of the style engine CSS rules, which expects a string.

Skip any query that is not a string, so `rules_group` is narrowed to `string` whichever class supplies the queries. Both implementations only return strings, so there is no change in behavior.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Revert "Navigation: Skip viewport media queries that are not strings"

This reverts commit aed1887.

The guard was only needed because Core's `WP_Theme_JSON::get_viewport_media_queries()` documented a plain `array` return. That method now has an array shape in WordPress/wordpress-develop#15644, so with stubs generated from it, each media query is already a `non-falsy-string` and `rules_group` is narrowed without the check.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* PHPStan: Read pull request stubs from the local checkout when it has the commit

The stubs generator already read a commit hash from the wordpress-develop checkout that Gutenberg is in, when the checkout has it. A pull request (`--pr`) or a branch (`--commit=trunk`), though, was always fetched from GitHub, since a ref is only resolved to a commit once fetched. That is slow even when the pull request's branch is the one checked out.

Resolve the ref to its commit first, so the checkout serves as a cache for these too. The lookup uses the GitHub REST API (`commits/{ref}` with the `vnd.github.sha` media type), which answers in under a second, and handles a branch, a tag, and `refs/pull/N/head` alike. `git ls-remote` was tried first, but for a repository with as many refs as wordpress-develop, it took around 50 seconds. When the lookup fails, or the checkout lacks the commit, the ref is fetched as before.

PHP's HTTP stream wrapper ignores the proxy environment variables that Git and curl read, so they are applied to the request, honoring `no_proxy`. Without that, the lookup fails wherever GitHub is only reachable through a proxy, falling back to the slow fetch. A `GITHUB_TOKEN` or `GH_TOKEN` is sent when set, for the higher rate limit of authenticated requests.

Generating the stubs from a pull request whose head is checked out drops from about 60 seconds to 6.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* PHPStan: Generate the WordPress stubs from WordPress/wordpress-develop#15644

That pull request adds the array shape for `WP_Theme_JSON::get_viewport_media_queries()`, which the Navigation block falls back to, so that its media queries are known to be strings when passed to the style engine as a `rules_group`.

Regenerate the stubs with `--commit=trunk` once it is committed to Core, before this is merged.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Revert "PHPStan: Read pull request stubs from the local checkout when it has the commit"

This reverts commit 07fd94e.

No network request is needed to reuse the local checkout for a pull request. With `--checkout --pr=<number>` and no uncommitted changes, the generator already records the checkout's `HEAD` commit together with the pull request, with the same description and source in composer.json as `--pr` gives, and without fetching anything.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* PHPStan: Document generating pull request stubs offline from the checkout

The usage and the coding guidelines only said that `--checkout` stubs cannot be generated again in CI, which holds for uncommitted changes alone. Without them, `--checkout --pr=<number>` records the checkout's commit along with the pull request, exactly as `--pr` does, so when the pull request's branch is checked out and pushed, it generates the same stubs without fetching the pull request from GitHub.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Docs: Break up the PHPStan paragraph in the coding guidelines

The whole PHPStan guidance was one paragraph, covering what is analyzed, how to run it, the baseline, the WordPress stubs, and local configuration. Give it a subsection of its own with a heading for each topic, put the commands in code blocks, and list the stub generator's options with when to use each. The content is unchanged.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* PHPStan: Move the viewport media queries ignore under a level 4 heading

The `function.alreadyNarrowedType` error for the Navigation block's `method_exists()` check is reported from rule level 4, but the entry was listed under the level 3 heading, after the `WP_REST_Request` offset entries. Give it a `Level 4:` heading, explain the check in the comment, and list `message` before `identifier`, as the other entries do.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Docs: List the PHPStan commands as alternatives

The commands to run the analysis and to regenerate the baseline were each shown together in one code block, which reads as if both should be run, when either one does. List them instead, each with the setup it is for.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Docs: Link the files and directories in the PHPStan guidance

Link each file and directory that the PHPStan subsection of the coding guidelines refers to, and name the stubs generator script that the Composer command runs.

Per the documentation guidelines, a package README is linked with a root-relative path, which the Block Editor Handbook rewrites. Other files and directories are linked by their full GitHub URLs, as `docs/manifest.json` is, since root-relative links to them would not resolve in the handbook.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Navigation: Accept any array of layout values instead of asserting string keys

The layouts merged for each viewport are narrowed from attribute values with `is_array()`, so PHPStan only knows them as `array<mixed>`. Passing their merge to a parameter typed `array<string, mixed>` needed an inline `@var` asserting string keys, which nothing checks.

`block_core_navigation_get_layout_custom_property_declarations()` does not depend on the keys being strings, since it only reads named offsets with `??`. Document `$layout` as `array<mixed>` instead, which is what it is given, and remove the `@var`, so that the call is the same as before.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

---------

Co-authored-by: huzaifaalmesbah <huzaifaalmesbah@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>
peterwilsoncc pushed a commit to peterwilsoncc/gutenberg-build that referenced this pull request Nov 6, 2026
* PHPStan: Resolve always-true type checks

- Drop the `method_exists()` check for `WP_Interactivity_API::add_client_navigation_support_to_script_module()` in `lib/client-assets.php`. The method was added in WordPress 6.9, and the plugin requires 7.0.
- Document the Navigation block's `$layout` parameter as `mixed`. It comes from block attributes, so the existing `is_array()` guards are needed.
- Ignore the `method_exists()` check for `WP_Theme_JSON_Gutenberg::get_viewport_media_queries()` in the Navigation block, which is needed when the block library runs in WordPress Core, where that class is not defined.

Remove the 4 resolved entries from the PHPStan baseline.

See #86088.

* Navigation: Narrow layout param instead of widening it to mixed

The only caller of `block_core_navigation_get_layout_custom_property_declarations()` passes the `array_replace()` of two values it has already checked with `is_array()`, so `$layout` is always an array. Document it as `array<string, mixed>` rather than `mixed`, and remove the three redundant `is_array( $layout )` guards. Each offset is still read with `??`, and `justifyContent` is still checked with `is_string()`, so unexpected values fall back to the defaults as before.

The return type is narrowed to `array<string, string>`.

At the call site, the merged layout gets an inline `@var array<string, mixed>`, since PHPStan types an array narrowed from an attribute as `array<mixed>`, while the layout attribute is a JSON object with string keys.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Navigation: Add a return shape for layout custom property declarations

Every key `block_core_navigation_get_layout_custom_property_declarations()` returns is fixed, and each value comes from a small set of literals, so document them in a `@phpstan-return` array shape.

For PHPStan to verify the `--navigation-layout-align` values, the vertical alignment is now looked up in its own map rather than chosen with `in_array()`. PHPStan does not tie the `in_array()` check to the `$justification` it picks, so it inferred that `space-between` could be returned. The behavior is unchanged: `center` and `right` map to `center` and `flex-end`, and everything else falls back to `flex-start`.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Code Quality: Type the script module registry entries

Annotate the `$modules` loaded in `gutenberg_define_interactivity_modules_support()` with the shape that wp-build's `generateModuleRegistrationPhp()` writes for each entry: an `id`, `path`, and `asset`, plus `min_only` set to `true` for modules that only ship minified, such as WASM workers. This resolves the `mixed` offset access errors at higher PHPStan levels.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Navigation: Combine the layout comment with its `@var` docblock

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Code Quality: Add a return shape for viewport media queries

`WP_Theme_JSON_Gutenberg::get_viewport_media_queries()` only ever returns the `@mobile`, `@tablet`, and `@desktop` keys, each holding a non-empty `@media` query string, so document that in a `@phpstan-return` array shape.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Navigation: Skip viewport media queries that are not strings

The media queries come from `WP_Theme_JSON_Gutenberg::get_viewport_media_queries()` or, when that is unavailable, from Core's `WP_Theme_JSON::get_viewport_media_queries()`. Core's method only documents a plain `array` return, so the union of the two leaves each `$media_query` as `mixed`. That value is passed as the `rules_group` of the style engine CSS rules, which expects a string.

Skip any query that is not a string, so `rules_group` is narrowed to `string` whichever class supplies the queries. Both implementations only return strings, so there is no change in behavior.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Revert "Navigation: Skip viewport media queries that are not strings"

This reverts commit aed18876670.

The guard was only needed because Core's `WP_Theme_JSON::get_viewport_media_queries()` documented a plain `array` return. That method now has an array shape in WordPress/wordpress-develop#15644, so with stubs generated from it, each media query is already a `non-falsy-string` and `rules_group` is narrowed without the check.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* PHPStan: Read pull request stubs from the local checkout when it has the commit

The stubs generator already read a commit hash from the wordpress-develop checkout that Gutenberg is in, when the checkout has it. A pull request (`--pr`) or a branch (`--commit=trunk`), though, was always fetched from GitHub, since a ref is only resolved to a commit once fetched. That is slow even when the pull request's branch is the one checked out.

Resolve the ref to its commit first, so the checkout serves as a cache for these too. The lookup uses the GitHub REST API (`commits/{ref}` with the `vnd.github.sha` media type), which answers in under a second, and handles a branch, a tag, and `refs/pull/N/head` alike. `git ls-remote` was tried first, but for a repository with as many refs as wordpress-develop, it took around 50 seconds. When the lookup fails, or the checkout lacks the commit, the ref is fetched as before.

PHP's HTTP stream wrapper ignores the proxy environment variables that Git and curl read, so they are applied to the request, honoring `no_proxy`. Without that, the lookup fails wherever GitHub is only reachable through a proxy, falling back to the slow fetch. A `GITHUB_TOKEN` or `GH_TOKEN` is sent when set, for the higher rate limit of authenticated requests.

Generating the stubs from a pull request whose head is checked out drops from about 60 seconds to 6.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* PHPStan: Generate the WordPress stubs from WordPress/wordpress-develop#15644

That pull request adds the array shape for `WP_Theme_JSON::get_viewport_media_queries()`, which the Navigation block falls back to, so that its media queries are known to be strings when passed to the style engine as a `rules_group`.

Regenerate the stubs with `--commit=trunk` once it is committed to Core, before this is merged.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Revert "PHPStan: Read pull request stubs from the local checkout when it has the commit"

This reverts commit 07fd94e7e86.

No network request is needed to reuse the local checkout for a pull request. With `--checkout --pr=<number>` and no uncommitted changes, the generator already records the checkout's `HEAD` commit together with the pull request, with the same description and source in composer.json as `--pr` gives, and without fetching anything.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* PHPStan: Document generating pull request stubs offline from the checkout

The usage and the coding guidelines only said that `--checkout` stubs cannot be generated again in CI, which holds for uncommitted changes alone. Without them, `--checkout --pr=<number>` records the checkout's commit along with the pull request, exactly as `--pr` does, so when the pull request's branch is checked out and pushed, it generates the same stubs without fetching the pull request from GitHub.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Docs: Break up the PHPStan paragraph in the coding guidelines

The whole PHPStan guidance was one paragraph, covering what is analyzed, how to run it, the baseline, the WordPress stubs, and local configuration. Give it a subsection of its own with a heading for each topic, put the commands in code blocks, and list the stub generator's options with when to use each. The content is unchanged.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* PHPStan: Move the viewport media queries ignore under a level 4 heading

The `function.alreadyNarrowedType` error for the Navigation block's `method_exists()` check is reported from rule level 4, but the entry was listed under the level 3 heading, after the `WP_REST_Request` offset entries. Give it a `Level 4:` heading, explain the check in the comment, and list `message` before `identifier`, as the other entries do.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Docs: List the PHPStan commands as alternatives

The commands to run the analysis and to regenerate the baseline were each shown together in one code block, which reads as if both should be run, when either one does. List them instead, each with the setup it is for.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Docs: Link the files and directories in the PHPStan guidance

Link each file and directory that the PHPStan subsection of the coding guidelines refers to, and name the stubs generator script that the Composer command runs.

Per the documentation guidelines, a package README is linked with a root-relative path, which the Block Editor Handbook rewrites. Other files and directories are linked by their full GitHub URLs, as `docs/manifest.json` is, since root-relative links to them would not resolve in the handbook.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

* Navigation: Accept any array of layout values instead of asserting string keys

The layouts merged for each viewport are narrowed from attribute values with `is_array()`, so PHPStan only knows them as `array<mixed>`. Passing their merge to a parameter typed `array<string, mixed>` needed an inline `@var` asserting string keys, which nothing checks.

`block_core_navigation_get_layout_custom_property_declarations()` does not depend on the keys being strings, since it only reads named offsets with `??`. Document `$layout` as `array<mixed>` instead, which is what it is given, and remove the `@var`, so that the call is the same as before.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>

---------

Co-authored-by: huzaifaalmesbah <huzaifaalmesbah@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>

Source: WordPress/gutenberg@8f625ba
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants