Skip to content

Style Engine: Improve PHPStan types and avoid fatal errors when storing rules - #15823

Open
westonruter wants to merge 6 commits into
WordPress:trunkfrom
westonruter:fix/style-engine-phpstan-types
Open

westonruter wants to merge 6 commits into
WordPress:trunkfrom
westonruter:fix/style-engine-phpstan-types

Conversation

@westonruter

@westonruter westonruter commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Improves the PHPStan types in WP_Style_Engine, bringing src/wp-includes/style-engine/class-wp-style-engine.php from 36 errors to none at level 10, and fixes a fatal error found along the way. This syncs the Style Engine changes from WordPress/gutenberg#86105 and WordPress/gutenberg#86468.

Types

  • Add a Style_Definition PHPStan type alias for one entry of WP_Style_Engine::BLOCK_STYLE_DEFINITIONS_METADATA, and use it for the constant and for every method that receives a definition.
  • Document raw $style_value parameters as mixed rather than string or array. parse_block_styles() passes whatever is found at a definition's path to every definition, so spacing and border radius values arrive as arrays, and numeric settings such as textColumns can be integers. The methods already check the type themselves.
  • Give get_classnames() a list<string> return type, and link the docblock references to {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}.

Behavior

  • WP_Style_Engine::store_css_rule() rejected an empty selector, but WP_Style_Engine_CSS_Rules_Store::add_rule() trims it and returns nothing when the result is empty. A selector of only whitespace therefore called add_declarations() on null, a fatal error reachable from wp_style_engine_get_stylesheet_from_css_rules() when a context is passed. It now skips storing the rule. A non-string store name fataled the same way, since the store lookup returned null.
  • get_css_declarations() and get_individual_property_css_declarations() now skip non-string values, so their documented string[] return type holds. WP_Style_Engine_CSS_Declarations::add_declaration() already dropped those values, so the compiled CSS is unchanged; they only leaked into the declarations array returned by wp_style_engine_get_styles().
  • A few small guards keep the analysis sound: the sibling definition lookup reads the constant directly instead of through _wp_array_get(), a definition without property_keys returns no declarations instead of reading an undefined index, and url() is only built from a string url.

New tests cover the whitespace selector and the non-string values.

The Navigation block change in the Gutenberg PR is not included here, since src/wp-includes/blocks/navigation.php is updated through the Gutenberg package bump.

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: Investigating the PHPStan errors, drafting the type changes, fixes, and tests, and syncing them between Gutenberg and Core; reviewed and directed 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.

westonruter and others added 5 commits November 7, 2026 09:13
`WP_Style_Engine::parse_block_styles()` passes the value found at each
definition's `path` to `is_valid_style_value()` and `get_classnames()`
for every definition, not just those with classnames. Those values are
often not strings: spacing and border radius values are arrays, font
weights and text columns can be numbers, and block attributes are
unvalidated. `get_slug_from_preset_value()` receives the same value and
already guards it with `is_string()`.

Document `$style_value` as `mixed` on all three methods instead of
`string`, matching `get_css_declarations()`. Also add PHPStan types to
`get_classnames()`: a shape for the `classnames` key it reads from
`$style_definition`, and a `list<string>` return type.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05
…ADATA

Wrap the docblock mentions of the constant in
`{@see self::BLOCK_STYLE_DEFINITIONS_METADATA}` so the developer docs
link to it, and qualify the mention in the inline comment in
`WP_Style_Engine::get_classnames()`.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05
Add a `Style_Definition` PHPStan type alias to `WP_Style_Engine`,
describing one entry of `BLOCK_STYLE_DEFINITIONS_METADATA`, and use it
for the constant and for every method that receives a definition. This
gives PHPStan the shapes of `property_keys`, `path`, `css_vars`,
`classnames`, and `value_func`.

Document the remaining raw style value parameters as `mixed`, since
`parse_block_styles()` passes whatever is found at the definition's
`path`, and the parsers already check its type.

With the types in place, a few small code changes keep the analysis
sound without changing behavior for valid definitions:

* Read the sibling definition in
  `get_individual_property_css_declarations()` from the constant
  directly, instead of through `_wp_array_get()`, which loses its type.
* Return early from `get_css_declarations()` when a definition has
  neither a callable `value_func` nor `property_keys`, rather than
  reading an undefined index.
* Only build a `url()` value in `get_url_or_value_css_declaration()`
  when the value is an array with a string `url`.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05
`WP_Style_Engine::store_css_rule()` rejects an empty selector, but
`WP_Style_Engine_CSS_Rules_Store::add_rule()` trims the selector and
returns nothing when the result is empty. A selector made only of
whitespace therefore passed the first check and then called
`add_declarations()` on null, which is a fatal error. It is reachable
from `wp_style_engine_get_stylesheet_from_css_rules()` when a `context`
is passed.

Skip storing the rule when no store or rule is returned, and declare the
method's `void` return type.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05
`get_css_declarations()` and `get_individual_property_css_declarations()`
are documented to return `string[]`, but they passed raw block style
values through unchanged, so a numeric value such as `textColumns: 2` or
a padding side of `5` ended up in the declarations as an integer.
`WP_Style_Engine_CSS_Declarations::add_declaration()` already drops
non-string values, so they never reached the compiled CSS; they only
leaked into the `declarations` array returned by
`wp_style_engine_get_styles()`.

Skip non-string values in both methods, so the return type holds. The
compiled CSS is unchanged.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05
@github-actions

github-actions Bot commented Nov 7, 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
westonruter requested a review from ramonjd November 9, 2026 22:48
@westonruter
westonruter marked this pull request as ready for review November 9, 2026 22:49
@github-actions

github-actions Bot commented Nov 9, 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.

@ramonjd ramonjd left a comment

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.

Thanks for taking care of these improvements.

'top' => 5,
'left' => '1em',
),
),

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.

Optional: Maybe add border.top.width: 5 to cover the individual properties for that style?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Could you amend the PR with what you have in mind?

/**
* Tests that non-string style values are left out of the returned declarations,
* since {@see WP_Style_Engine_CSS_Declarations::add_declaration()} would drop them from the CSS anyway.
*

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.

Not sure if this and above needs @ticket

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Since the only ticket is the catch-all Core-65817 for PHPStan fixes, I don't think it would be helpful.

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