Skip to content

Narrow types and sync WP_Theme_JSON_Gutenberg::get_feature_declarations_for_node() with core - #68407

Merged
westonruter merged 15 commits into
WordPress:trunkfrom
justlevine:docs/metadata-param-type_get_feature_declarations_for_node
Oct 26, 2026
Merged

westonruter merged 15 commits into
WordPress:trunkfrom
justlevine:docs/metadata-param-type_get_feature_declarations_for_node

Conversation

@justlevine

Copy link
Copy Markdown
Contributor

What?

This PR fixes the $metadata param type for WP_Theme_JSON_Gutenberg::get_feature_declarations_for_node() so it correctly expects an array type instead of a object.

Why?

While this issue was surfaced by PHPStan (via #66693) it can be remediated independently as part of #66598

How?

Testing Instructions

Testing Instructions for Keyboard

Screenshots or screencast

Before After

@carolinan carolinan added [Type] Code Quality Issues or PRs that relate to code quality Needs PHP backport Needs PHP backport to Core labels Jan 27, 2025
@carolinan

Copy link
Copy Markdown
Contributor

@justlevine
justlevine force-pushed the docs/metadata-param-type_get_feature_declarations_for_node branch from 2f799a2 to dd4f7d3 Compare April 18, 2025 23:06
westonruter and others added 5 commits October 24, 2026 21:54
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both parameters of `get_feature_declarations_for_node()` only ever receive
arrays. This syncs the `@param` tags with `WP_Theme_JSON` in WordPress
core as of r63426, which corrected both from `object` and noted that the
by-reference `$node` has its promoted features removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the shapes `WP_Theme_JSON_Gutenberg::get_feature_declarations_for_node()`
works with: the `selectors` it reads from the block metadata map each
feature to a selector string or to a map of subfeature selectors, the node
is a theme.json node keyed by feature, and the returned declarations are
keyed by selector. Each declaration list uses the same shape that
`get_state_declarations_with_text_clip_escapes()` already documents.

This resolves the PHPStan errors on mixed offsets and iteration inside the
method at the higher rule levels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block metadata passed to `get_feature_declarations_for_node()` is a
string-keyed array, just like the node, so say so in the plain `@param`
tag as well. The `@phpstan-param` shape still describes the `selectors`
key the method reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
westonruter and others added 9 commits October 25, 2026 14:09
The fix is being brought to core in WordPress/wordpress-develop#14862,
along with the related PHPStan fixes from three other Gutenberg PRs, which
share this entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`get_feature_declarations_for_node()` only checked that the feature's
selectors were an array before reading `$node[ $feature ][ $subfeature ]`,
but the node comes from theme.json data, where the feature value may be a
string or another non-array value. Such a value has no subfeatures, and
`empty()` already treated every lookup on it as empty, so skipping the
subfeature loop for it keeps the behavior the same while making the check
explicit.

This resolves the PHPStan error for accessing an offset on `mixed`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checking `is_array( $node[ $feature ] )` was not enough for PHPStan,
since unsetting `$node[ $feature ][ $subfeature ]` inside the loop made it
forget the narrowed type on the next iteration. Copy the feature styles to
a local variable, read and unset the subfeatures there, and write the copy
back to the node after the loop, which leaves the node exactly as before.

This resolves the PHPStan error for accessing an offset on `mixed`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the theme.json data has no `settings`, `get_styles_for_block()` and
`get_feature_declarations_for_node()` fell back to `null`, which was
then passed on as the settings to `compute_style_properties()` and the
other style helpers, all of which expect an array. Core has always fallen
back to an empty array here, so do the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Give the `$theme_json` property a `@phpstan-var` shape for its top-level
`version`, `settings`, and `styles` keys, leaving it open to the others.
Reading `$this->theme_json['settings']` or `['styles']` then yields an
array rather than `mixed`, which resolves the PHPStan errors at the higher
rule levels where these are passed to `compute_style_properties()` and the
other style helpers, or have offsets read from them.

The methods that build the data still return a plain `array`, so the
assignments to the property are now reported instead, until those return
types are narrowed as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Give `compute_style_properties()` a `@phpstan-return` with the same
declaration shape that `get_state_declarations_with_text_clip_escapes()`
documents. Its callers, including `get_feature_declarations_for_node()`,
then see a list of declarations rather than a plain array, which resolves
their PHPStan errors at the higher rule levels.

This moves some errors elsewhere at those levels: to the method's own body, whose property
names come from the untyped `$properties` parameter and whose font size
value may be `null` as far as PHPStan can tell, and to the callers that pass
a declaration value to `is_safe_css_declaration()`, which expects a string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Now that `get_styles_for_block()` falls back to an empty settings array,
`$settings` is never `null`, so the `?? array()` when passing it to
`get_state_base_styles()` is redundant. Drop it, as core does.

This resolves the PHPStan error that the variable on the left side of
`??` always exists and is not nullable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring over the documentation differences from
`WP_Theme_JSON::get_feature_declarations_for_node()` in core: the
punctuation in the summary, the `@since` tag, no blank line between the
`@param` and `@return` tags, and multi-line comments in the `/* */` style
instead of consecutive `//` lines. The method is now identical to core's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `@since 6.3.0` tag brought over from core refers to the WordPress
release that added the method, which is meaningless for the plugin's copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@westonruter

Copy link
Copy Markdown
Member

🤖 Comment by Claude Opus 5.5

trunk was merged in, and the change was extended beyond the original fix so that get_feature_declarations_for_node() has no PHPStan errors left even at level 10:

  • f0bb2db corrects $node as well as $metadata from object to array, matching core after r63426, and notes that features promoted to their own selector are removed from $node, since the method unset()s them.
  • e735da8 and 50300ee narrow the types: both parameters are array<string, mixed>, a @phpstan-param shape describes the selectors key the method reads, and the return value is keyed by selector, with each declaration list using the list<array{ name: non-falsy-string, value: scalar }> shape that get_state_declarations_with_text_clip_escapes() already documents.
  • 546eee9 and e846c3f check that $node[ $feature ] is an array before reading its subfeatures, and read them from a local copy that is written back after the loop. Otherwise PHPStan forgets the narrowed type after the unset() inside the loop. Behavior is unchanged, since empty() already treated every lookup on a non-array value as empty.
  • 84b1fa6 and e07caca fall back to an empty array rather than null when the theme.json data has no settings, as core has always done, which makes a ?? array() further down redundant.
  • 47d125e gives the $theme_json property a shape for its version, settings, and styles keys, so that $this->theme_json['settings'] is an array rather than mixed.
  • e4633c4 gives compute_style_properties() a @phpstan-return with the same declaration shape.
  • f5f13db and fedefec bring over the remaining documentation differences from core (summary punctuation, no blank line before @return, and /* */ comments), so the method now matches WP_Theme_JSON::get_feature_declarations_for_node() in core with Sync PHPStan fixes from Gutenberg for block supports, duotone, and theme.json wordpress-develop#14862 applied, apart from core's @since tag.

At PHPStan level 5 this fixes 12 errors and introduces none. At level 10 the total drops from 3183 to 3136. The last two commits move some errors to the boundaries: assignments to $theme_json from methods that still return a plain array, and declaration values passed to is_safe_css_declaration(), which expects a string. Those are left for a follow-up.

Since this goes beyond r63426, it is being brought to core in WordPress/wordpress-develop#14862, along with three related PHPStan fixes, under Core-65817; the backport changelog entry was added for it. WP_Theme_JSON_Gutenberg_Test passes.

@github-actions

Copy link
Copy Markdown

🤖 PR meta 🤖

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: justlevine <justlevine@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>
Co-authored-by: carolinan <poena@git.wordpress.org>

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.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

@westonruter westonruter changed the title docs: fix $metadata param type in WP_Theme_JSON_Gutenberg::get_feature_declarations_for_node() Narrow types and sync WP_Theme_JSON_Gutenberg::get_feature_declarations_for_node() with core Oct 25, 2026
@westonruter

Copy link
Copy Markdown
Member

Difference between core and gutenberg's implementations of get_feature_declarations_for_node now between this PR and WordPress/wordpress-develop#14862:

--- gutenberg.txt	2026-10-25 15:29:04
+++ core.txt	2026-10-25 15:29:20
@@ -3,6 +3,8 @@
 	 * typography etc. that have custom selectors in their related block's
 	 * metadata.
 	 *
+	 * @since 6.3.0
+	 *
 	 * @param array<string, mixed> $metadata The related block metadata containing selectors.
 	 * @param array<string, mixed> $node     A merged theme.json node for block or variation. Features
 	 *                                       promoted to their own selector are removed from it.

@westonruter

Copy link
Copy Markdown
Member

🤖 Comment by Claude Opus 5.5

A clarification on the $this->theme_json['settings'] ?? null → ?? array() change in 84b1fa6: its commit message says core "has always fallen back to an empty array here", but that is not quite accurate. The history is:

Date Change Fallback for $settings
Before September 2023 Both Gutenberg and core used _wp_array_get( $this->theme_json, array( 'settings' ) ) null, via the default of _wp_array_get()'s $default parameter
September 19, 2023 Gutenberg #51116 replaced the _wp_array_get() calls $this->theme_json['settings'] ?? null in get_styles_for_block() and get_feature_declarations_for_node(), keeping the implicit null; a third spot became ?? array()
September 26, 2023 Core r56709 (Core-59405) made the same change in core isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array() in all three places
2025 Core r61431 (Core-63430) converted those ternaries to ?? $this->theme_json['settings'] ?? array()

So both originally fell back to null, and the two codebases diverged when the _wp_array_get() calls were replaced a week apart. Core has used array() since r56709, while Gutenberg kept null in two places until this PR. An empty array is the better fallback, since compute_style_properties() and the other helpers that receive $settings expect an array. That in turn made the $settings ?? array() passed to get_state_base_styles() redundant, which e07caca removes, matching core.

$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
$selector = $block_metadata['selector'];
$settings = $this->theme_json['settings'] ?? null;
$settings = $this->theme_json['settings'] ?? array();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

Core has used an array() fallback here since r56709, while Gutenberg kept null from #51116. See #68407 (comment) for the full history.

@westonruter

Copy link
Copy Markdown
Member

🤖 Comment by Claude Opus 5.5

Similarly, the history of the get_state_base_styles( …, $settings ) call in get_styles_for_block(), whose ?? array() e07caca removes:

Date Change Third argument
October 13, 2026 Gutenberg #77142 adds get_state_base_styles(), with untyped parameters static::get_state_base_styles( …, $settings )
October 20, 2026 Gutenberg #85057 adds native types: get_state_base_styles( array $path, array $tree, array $settings ): array self::get_state_base_styles( …, $settings ?? array() )
October 20, 2026 Core r64590 (Core-66094) brings in #77141, #77142, and #85057 self::get_state_base_styles( …, $settings ), with the same native array $settings type
This PR 84b1fa6 and e07caca $settings, matching core

The ?? array() was only needed in Gutenberg. With the native array type added in #85057, passing null would throw a TypeError, and Gutenberg's $settings could be null because of the $this->theme_json['settings'] ?? null fallback described in #68407 (comment). In core, $settings has fallen back to array() since r56709, so r64590 passes it as is.

So the call-site ?? array() worked around the null fallback rather than fixing it. Once 84b1fa6 changed the fallback to array(), PHPStan reported the call-site fallback as redundant at level 5 (nullCoalesce.variable: "Variable $settings on left side of ?? always exists and is not nullable."), so e07caca removed it.

* Otherwise just compute the styles for the default selector as normal.
*/
$base_styles = self::get_state_base_styles( $block_metadata['path'] ?? array(), $this->theme_json, $settings ?? array() );
$base_styles = self::get_state_base_styles( $block_metadata['path'] ?? array(), $this->theme_json, $settings );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

The ?? array() here was added in #85057 to work around $settings possibly being null; core passes $settings as is. See #68407 (comment) for the full history.

}

$settings = $this->theme_json['settings'] ?? null;
$settings = $this->theme_json['settings'] ?? array();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

Same as in get_styles_for_block(): core has used an array() fallback here since r56709, while Gutenberg kept null from #51116. See #68407 (comment) for the full history.

@westonruter

Copy link
Copy Markdown
Member

@westonruter
westonruter merged commit dd6d865 into WordPress:trunk Oct 26, 2026
94 checks passed
@github-actions github-actions Bot added this to the Gutenberg 24.4 milestone Oct 26, 2026
wporg-sync pushed a commit to WordPress/wordpress-develop that referenced this pull request Oct 26, 2026
These fixes were made in Gutenberg while resolving the PHPStan errors in the plugin, ahead of adding PHPStan to its CI, and are brought to core to keep the two codebases in sync. They were developed in:

* WordPress/gutenberg#68398
* WordPress/gutenberg#68404
* WordPress/gutenberg#68405
* WordPress/gutenberg#68407

The first one fixes a bug in core. Since r56046 the `array_key_exists()` checks in `wp_register_shadow_support()` have lacked their negation, so a block type with shadow support but no `style` or `shadow` attribute never had them registered, while one that already defined them had its definitions replaced, dropping any `default`. Gutenberg already had the negation. Tests are added for the attribute registration.

The others make implicit type conversions explicit in the layout and duotone code, and narrow the types used by `WP_Theme_JSON::get_feature_declarations_for_node()` so that it reports no errors even at PHPStan rule level 10. Since the node comes from `theme.json` data, that method now also checks that a feature is an array before reading its subfeatures. This fixes a fatal error ("Cannot unset string offsets") when a feature's value is a string, such as a `shadow`, and the block's selectors for that feature are a list rather than keyed by subfeature name, since each numeric key then reads a character of the string instead of a subfeature. The baseline entries for the resolved errors are removed.

Developed in #14862.
Follow-up to r56046, r56058, r56101, r62478, r63426.

Props justlevine, westonruter, poena, ramonopoly.
See #65817.


git-svn-id: https://develop.svn.wordpress.org/trunk@64727 602fd350-edb4-49c9-b593-d223f7449a82
wporg-sync pushed a commit to WordPress/WordPress that referenced this pull request Oct 26, 2026
These fixes were made in Gutenberg while resolving the PHPStan errors in the plugin, ahead of adding PHPStan to its CI, and are brought to core to keep the two codebases in sync. They were developed in:

* WordPress/gutenberg#68398
* WordPress/gutenberg#68404
* WordPress/gutenberg#68405
* WordPress/gutenberg#68407

The first one fixes a bug in core. Since r56046 the `array_key_exists()` checks in `wp_register_shadow_support()` have lacked their negation, so a block type with shadow support but no `style` or `shadow` attribute never had them registered, while one that already defined them had its definitions replaced, dropping any `default`. Gutenberg already had the negation. Tests are added for the attribute registration.

The others make implicit type conversions explicit in the layout and duotone code, and narrow the types used by `WP_Theme_JSON::get_feature_declarations_for_node()` so that it reports no errors even at PHPStan rule level 10. Since the node comes from `theme.json` data, that method now also checks that a feature is an array before reading its subfeatures. This fixes a fatal error ("Cannot unset string offsets") when a feature's value is a string, such as a `shadow`, and the block's selectors for that feature are a list rather than keyed by subfeature name, since each numeric key then reads a character of the string instead of a subfeature. The baseline entries for the resolved errors are removed.

Developed in WordPress/wordpress-develop#14862.
Follow-up to r56046, r56058, r56101, r62478, r63426.

Props justlevine, westonruter, poena, ramonopoly.
See #65817.

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


git-svn-id: http://core.svn.wordpress.org/trunk@63877 1a063a9b-81f0-0310-95a4-ce76da25c4cd
@ingeniumed ingeniumed added the Global Styles Anything related to the broader Global Styles efforts, including Styles Engine and theme.json label Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Global Styles Anything related to the broader Global Styles efforts, including Styles Engine and theme.json Needs PHP backport Needs PHP backport to Core [Type] Code Quality Issues or PRs that relate to code quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants