From e73cd00b2c6ca68c8f37be58645245a26b95575d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 7 Nov 2026 09:13:02 -0700 Subject: [PATCH 1/5] Style Engine: Document raw style values as mixed `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` return type. Co-Authored-By: Claude Opus 5.5 (medium effort) Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05 --- src/wp-includes/style-engine/class-wp-style-engine.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index 81499d15e5905..72c1bed7c9e21 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -403,7 +403,7 @@ final class WP_Style_Engine { * * @since 6.1.0 * - * @param string $style_value A single CSS preset value. + * @param mixed $style_value A single CSS preset value. * @param string $property_key The CSS property that is the second element of the preset string. * Used for matching. * @return string The slug, or empty string if not found. @@ -448,7 +448,7 @@ protected static function get_css_var_value( $style_value, $css_vars ) { * * @since 6.1.0 * - * @param string $style_value A single CSS preset value. + * @param mixed $style_value A single raw style value. * @return bool */ protected static function is_valid_style_value( $style_value ) { @@ -567,10 +567,13 @@ public static function parse_block_styles( $block_styles, $options ) { * * @since 6.1.0 * - * @param string $style_value A single raw style value or CSS preset property + * @param mixed $style_value A single raw style value or CSS preset property * from the `$block_styles` array. * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. * @return string[] An array of CSS classnames, or empty array if there are none. + * + * @phpstan-param array{ classnames?: array, ... } $style_definition + * @phpstan-return list */ protected static function get_classnames( $style_value, $style_definition ) { if ( empty( $style_value ) ) { From bf4a6e969ff36cb5239e39d6c5a8f7bea9833c64 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 7 Nov 2026 09:20:49 -0700 Subject: [PATCH 2/5] Style Engine: Link docblock references to BLOCK_STYLE_DEFINITIONS_METADATA 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) Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05 --- .../style-engine/class-wp-style-engine.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index 72c1bed7c9e21..d0aab9470de6b 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -495,7 +495,7 @@ public static function get_store( $store_name ) { /** * Returns classnames and CSS based on the values in a styles object. * - * Return values are parsed based on the instructions in BLOCK_STYLE_DEFINITIONS_METADATA. + * Return values are parsed based on the instructions in {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * * @since 6.1.0 * @@ -569,7 +569,7 @@ public static function parse_block_styles( $block_styles, $options ) { * * @param mixed $style_value A single raw style value or CSS preset property * from the `$block_styles` array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return string[] An array of CSS classnames, or empty array if there are none. * * @phpstan-param array{ classnames?: array, ... } $style_definition @@ -592,7 +592,7 @@ protected static function get_classnames( $style_value, $style_definition ) { if ( $slug ) { /* - * Right now we expect a classname pattern to be stored in BLOCK_STYLE_DEFINITIONS_METADATA. + * Right now we expect a classname pattern to be stored in self::BLOCK_STYLE_DEFINITIONS_METADATA. * One day, if there are no stored schemata, we could allow custom patterns or * generate classnames based on other properties * such as a path or a value or a prefix passed in options. @@ -611,7 +611,7 @@ protected static function get_classnames( $style_value, $style_definition ) { * @since 6.1.0 * * @param mixed $style_value A single raw style value from $block_styles array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @param array $options { * Optional. An array of options. Default empty array. * @@ -693,7 +693,7 @@ protected static function get_css_declarations( $style_value, $style_definition, * @since 6.1.0 * * @param array $style_value A single raw style value from `$block_styles` array. - * @param array $individual_property_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA + * @param array $individual_property_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA} * representing an individual property of a CSS property, * e.g. 'top' in 'border-top'. * @param array $options { @@ -757,7 +757,7 @@ protected static function get_individual_property_css_declarations( $style_value * @since 6.4.0 * * @param array $style_value A single raw style value from $block_styles array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return string[] An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). */ protected static function get_url_or_value_css_declaration( $style_value, $style_definition ) { @@ -793,7 +793,7 @@ protected static function get_url_or_value_css_declaration( $style_value, $style * @since 7.2.0 * * @param string $style_value A single raw style value from $block_styles array. - * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return array An associative array of CSS definitions, mapping properties to values. * * @phpstan-return array From 905959216871ccd9a47fde33f55b4ad47d85a834 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 7 Nov 2026 09:36:36 -0700 Subject: [PATCH 3/5] Style Engine: Add a PHPStan type for style definitions 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) Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05 --- .../style-engine/class-wp-style-engine.php | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index d0aab9470de6b..706f6d0985515 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -28,6 +28,14 @@ * @since 6.7.0 Added support for typography.writingMode. * @since 7.0.0 Added support for typography.textIndent. * @since 7.2.0 Added support for typography.textShadow. + * + * @phpstan-type Style_Definition array{ + * property_keys?: array{ default: string, individual?: string }, + * path: non-empty-list, + * css_vars?: array, + * classnames?: array, + * value_func?: callable( mixed, array, array ): array, + * } */ #[AllowDynamicProperties] final class WP_Style_Engine { @@ -59,7 +67,8 @@ final class WP_Style_Engine { * @since 6.1.0 * @since 7.1.0 Added `background.gradient` property. * @since 7.2.0 Added `background.backgroundClip` property. - * @var array + * @var array> + * @phpstan-var array> */ const BLOCK_STYLE_DEFINITIONS_METADATA = array( 'background' => array( @@ -499,8 +508,8 @@ public static function get_store( $store_name ) { * * @since 6.1.0 * - * @param array $block_styles The style object. - * @param array $options { + * @param array $block_styles The style object. + * @param array $options { * Optional. An array of options. Default empty array. * * @type bool $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns, @@ -572,7 +581,7 @@ public static function parse_block_styles( $block_styles, $options ) { * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return string[] An array of CSS classnames, or empty array if there are none. * - * @phpstan-param array{ classnames?: array, ... } $style_definition + * @phpstan-param Style_Definition $style_definition * @phpstan-return list */ protected static function get_classnames( $style_value, $style_definition ) { @@ -620,12 +629,18 @@ protected static function get_classnames( $style_value, $style_definition ) { * to `var( --wp--preset--* )` values. Default false. * } * @return string[] An associative array of CSS definitions, e.g. `array( "$property" => "$value", "$property" => "$value" )`. + * + * @phpstan-param Style_Definition $style_definition */ protected static function get_css_declarations( $style_value, $style_definition, $options = array() ) { if ( isset( $style_definition['value_func'] ) && is_callable( $style_definition['value_func'] ) ) { return call_user_func( $style_definition['value_func'], $style_value, $style_definition, $options ); } + if ( ! isset( $style_definition['property_keys'] ) ) { + return array(); + } + $css_declarations = array(); $style_property_keys = $style_definition['property_keys']; $should_skip_css_vars = isset( $options['convert_vars_to_classnames'] ) && true === $options['convert_vars_to_classnames']; @@ -692,7 +707,7 @@ protected static function get_css_declarations( $style_value, $style_definition, * * @since 6.1.0 * - * @param array $style_value A single raw style value from `$block_styles` array. + * @param mixed $style_value A single raw style value from `$block_styles` array. * @param array $individual_property_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA} * representing an individual property of a CSS property, * e.g. 'top' in 'border-top'. @@ -704,6 +719,8 @@ protected static function get_css_declarations( $style_value, $style_definition, * to `var( --wp--preset--* )` values. Default false. * } * @return string[] An associative array of CSS definitions, e.g. `array( "$property" => "$value", "$property" => "$value" )`. + * + * @phpstan-param Style_Definition $individual_property_definition */ protected static function get_individual_property_css_declarations( $style_value, $individual_property_definition, $options = array() ) { if ( ! is_array( $style_value ) || empty( $style_value ) || empty( $individual_property_definition['path'] ) ) { @@ -728,9 +745,8 @@ protected static function get_individual_property_css_declarations( $style_value continue; } - // Build a path to the individual rules in definitions. - $style_definition_path = array( $definition_group_key, $css_property ); - $style_definition = _wp_array_get( static::BLOCK_STYLE_DEFINITIONS_METADATA, $style_definition_path, null ); + // Get the corresponding style definition from the same group, e.g. `border.color`. + $style_definition = static::BLOCK_STYLE_DEFINITIONS_METADATA[ $definition_group_key ][ $css_property ] ?? null; if ( $style_definition && isset( $style_definition['property_keys']['individual'] ) ) { // Set a CSS var if there is a valid preset value. @@ -756,9 +772,12 @@ protected static function get_individual_property_css_declarations( $style_value * * @since 6.4.0 * - * @param array $style_value A single raw style value from $block_styles array. + * @param mixed $style_value A single raw style value from $block_styles array. * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return string[] An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + * + * @phpstan-param Style_Definition $style_definition + * @phpstan-return array */ protected static function get_url_or_value_css_declaration( $style_value, $style_definition ) { if ( empty( $style_value ) ) { @@ -770,7 +789,7 @@ protected static function get_url_or_value_css_declaration( $style_value, $style if ( isset( $style_definition['property_keys']['default'] ) ) { $value = null; - if ( ! empty( $style_value['url'] ) ) { + if ( is_array( $style_value ) && ! empty( $style_value['url'] ) && is_string( $style_value['url'] ) ) { $value = "url('" . $style_value['url'] . "')"; } elseif ( is_string( $style_value ) ) { $value = $style_value; @@ -792,10 +811,11 @@ protected static function get_url_or_value_css_declaration( $style_value, $style * * @since 7.2.0 * - * @param string $style_value A single raw style value from $block_styles array. - * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. + * @param mixed $style_value A single raw style value from $block_styles array. + * @param array $style_definition A single style definition from {@see self::BLOCK_STYLE_DEFINITIONS_METADATA}. * @return array An associative array of CSS definitions, mapping properties to values. * + * @phpstan-param Style_Definition $style_definition * @phpstan-return array */ protected static function get_background_clip_css_declarations( $style_value, $style_definition ) { From d36b032d1088a5d4d5fa9364da995dbfbedf3314 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 7 Nov 2026 09:37:29 -0700 Subject: [PATCH 4/5] Style Engine: Avoid a fatal error for a whitespace-only selector `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) Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05 --- .../style-engine/class-wp-style-engine.php | 14 +++++++++-- .../tests/style-engine/styleEngine.php | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index 706f6d0985515..4e582869bbb1e 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -482,11 +482,21 @@ protected static function is_valid_style_value( $style_value ) { * or a CSS nested @rule, such as `@media (min-width: 80rem)` * or `@layer module`. */ - public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ) { + public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ): void { if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) { return; } - static::get_store( $store_name )->add_rule( $css_selector, $rules_group )->add_declarations( $css_declarations ); + + $store = static::get_store( $store_name ); + if ( ! $store ) { + return; + } + + // The rule is null when the selector is only whitespace. + $rule = $store->add_rule( $css_selector, $rules_group ); + if ( $rule ) { + $rule->add_declarations( $css_declarations ); + } } /** diff --git a/tests/phpunit/tests/style-engine/styleEngine.php b/tests/phpunit/tests/style-engine/styleEngine.php index af6444d9bf88d..8d64e843cea80 100644 --- a/tests/phpunit/tests/style-engine/styleEngine.php +++ b/tests/phpunit/tests/style-engine/styleEngine.php @@ -995,4 +995,28 @@ public function test_should_return_stylesheet_with_nested_rules() { $this->assertSame( '.foo{@media (orientation: landscape){background-color:blue;}}.foo{@media (min-width > 1024px){background-color:cotton-blue;}}', $compiled_stylesheet ); } + + /** + * Tests that storing a rule whose selector is only whitespace is skipped + * instead of calling {@see WP_Style_Engine_CSS_Rule::add_declarations()} on null. + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + * @covers WP_Style_Engine::store_css_rule + */ + public function test_should_skip_storing_rule_with_whitespace_selector() { + $css_rules = array( + array( + 'selector' => ' ', + 'declarations' => array( + 'color' => 'red', + ), + ), + ); + + wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'context' => 'test-whitespace-selector' ) ); + + $store = WP_Style_Engine::get_store( 'test-whitespace-selector' ); + $this->assertInstanceOf( WP_Style_Engine_CSS_Rules_Store::class, $store ); + $this->assertSame( array(), $store->get_all_rules(), 'Expected no rule to be stored.' ); + } } From 72aa5ae2d740518d882ca3b94d9749f9a20ee225 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 7 Nov 2026 09:48:40 -0700 Subject: [PATCH 5/5] Style Engine: Only return string values as CSS declarations `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) Claude-Code-Session-Id: 1064defd-b2b0-40d4-bfca-0c5b60899c05 --- .../style-engine/class-wp-style-engine.php | 12 ++++--- .../tests/style-engine/styleEngine.php | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index 4e582869bbb1e..2ccfdefa95e0b 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -687,7 +687,7 @@ protected static function get_css_declarations( $style_value, $style_definition, $individual_property = sprintf( $style_property_keys['individual'], _wp_to_kebab_case( $key ) ); - if ( $individual_property && static::is_valid_style_value( $value ) ) { + if ( $individual_property && is_string( $value ) && static::is_valid_style_value( $value ) ) { $css_declarations[ $individual_property ] = $value; } } @@ -695,7 +695,10 @@ protected static function get_css_declarations( $style_value, $style_definition, return $css_declarations; } - $css_declarations[ $style_property_keys['default'] ] = $style_value; + // CSS declarations only accept string values. + if ( is_string( $style_value ) ) { + $css_declarations[ $style_property_keys['default'] ] = $style_value; + } return $css_declarations; } @@ -751,7 +754,8 @@ protected static function get_individual_property_css_declarations( $style_value $css_declarations = array(); foreach ( $style_value as $css_property => $value ) { - if ( empty( $value ) ) { + // CSS declarations only accept string values. + if ( empty( $value ) || ! is_string( $value ) ) { continue; } @@ -760,7 +764,7 @@ protected static function get_individual_property_css_declarations( $style_value if ( $style_definition && isset( $style_definition['property_keys']['individual'] ) ) { // Set a CSS var if there is a valid preset value. - if ( is_string( $value ) && str_contains( $value, 'var:' ) + if ( str_contains( $value, 'var:' ) && ! $should_skip_css_vars && ! empty( $individual_property_definition['css_vars'] ) ) { $value = static::get_css_var_value( $value, $individual_property_definition['css_vars'] ); diff --git a/tests/phpunit/tests/style-engine/styleEngine.php b/tests/phpunit/tests/style-engine/styleEngine.php index 8d64e843cea80..3cd23ef57d807 100644 --- a/tests/phpunit/tests/style-engine/styleEngine.php +++ b/tests/phpunit/tests/style-engine/styleEngine.php @@ -1019,4 +1019,36 @@ public function test_should_skip_storing_rule_with_whitespace_selector() { $this->assertInstanceOf( WP_Style_Engine_CSS_Rules_Store::class, $store ); $this->assertSame( array(), $store->get_all_rules(), 'Expected no rule to be stored.' ); } + + /** + * 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. + * + * @covers ::wp_style_engine_get_styles + */ + public function test_should_omit_non_string_values_from_declarations() { + $block_styles = array( + 'typography' => array( + 'textColumns' => 2, + 'fontSize' => '12px', + ), + 'spacing' => array( + 'padding' => array( + 'top' => 5, + 'left' => '1em', + ), + ), + ); + + $this->assertSame( + array( + 'css' => 'padding-left:1em;font-size:12px;', + 'declarations' => array( + 'padding-left' => '1em', + 'font-size' => '12px', + ), + ), + wp_style_engine_get_styles( $block_styles ) + ); + } }