From 1c3f1a6217dc0626c99fd2910d4cb8ef1ce6b9c3 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 8 Oct 2026 08:36:37 +0000 Subject: [PATCH 1/4] Docs: Add PHPStan purity annotations. Mark functions whose result depends only on their arguments as `@phpstan-pure`: `add_cssclass()`, `backslashit()`, `trailingslashit()`, `untrailingslashit()`, `wp_slash()`, `bool_from_yn()`, `wp_is_numeric_array()`, `rest_sanitize_boolean()` and `get_shortcode_atts_regex()`. PHPStan then verifies that these functions have no side effects, and reports calls whose result is unused. It also remembers their return values when `rememberPossiblyImpureFunctionValues` is disabled. See #65817. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T3XYNUvB9sQNDXA14uJQeH --- src/wp-admin/includes/menu.php | 1 + src/wp-includes/formatting.php | 7 +++++++ src/wp-includes/functions.php | 2 ++ src/wp-includes/rest-api.php | 1 + src/wp-includes/shortcodes.php | 1 + 5 files changed, 12 insertions(+) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index f1c2985e93e8e..1e71c782ec8ea 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -211,6 +211,7 @@ * * @phpstan-template T of string * @phpstan-param T $class_to_add + * @phpstan-pure * @phpstan-return ( $classes is empty ? T : non-empty-string ) */ function add_cssclass( $class_to_add, $classes ) { diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 552c5f17c21a3..3e01bfc4de679 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -2855,6 +2855,8 @@ function zeroise( $number, $threshold ) { * * @param string $value Value to which backslashes will be added. * @return string String with backslashes inserted. + * + * @phpstan-pure */ function backslashit( $value ) { if ( isset( $value[0] ) && $value[0] >= '0' && $value[0] <= '9' ) { @@ -2878,6 +2880,7 @@ function backslashit( $value ) { * @return string String with trailing slash added. * * @phpstan-return non-falsy-string + * @phpstan-pure */ function trailingslashit( $value ) { return untrailingslashit( $value ) . '/'; @@ -2893,6 +2896,8 @@ function trailingslashit( $value ) { * * @param string $value Value from which trailing slashes will be removed. * @return string String without the trailing slashes. + * + * @phpstan-pure */ function untrailingslashit( $value ) { return rtrim( $value, '/\\' ); @@ -5945,6 +5950,8 @@ function sanitize_trackback_urls( $to_ping ) { * T is array ? array, ( value-of is string ? string : value-of )> : T * ) * ) + * + * @phpstan-pure */ function wp_slash( $value ) { if ( is_array( $value ) ) { diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 114a9484f5659..5169927abd9ff 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -1616,6 +1616,7 @@ function get_num_queries() { * @param string $yn Character string containing either 'y' (yes) or 'n' (no). * @return bool True if 'y', false on anything else. * + * @phpstan-pure * @phpstan-return ( $yn is 'y'|'Y' ? true : false ) */ function bool_from_yn( $yn ) { @@ -5444,6 +5445,7 @@ function _wp_to_kebab_case( $input_string ) { * * @phpstan-assert-if-true array $data * + * @phpstan-pure * @phpstan-return ( $data is array ? true : false ) */ function wp_is_numeric_array( $data ): bool { diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 36b5b23de9f00..dbfbbeaeb576d 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1534,6 +1534,7 @@ function rest_is_ip_address( $ip ) { * @param bool|string|int $value The value being evaluated. * @return bool Returns the proper associated boolean value. * + * @phpstan-pure * @phpstan-return ( * $value is false|''|'0'|0|'false'|'False'|'FALSE' * ? false diff --git a/src/wp-includes/shortcodes.php b/src/wp-includes/shortcodes.php index bd5f74a1ec17b..3ee3bd771072b 100644 --- a/src/wp-includes/shortcodes.php +++ b/src/wp-includes/shortcodes.php @@ -597,6 +597,7 @@ function unescape_invalid_shortcodes( $content ) { * @return string The shortcode attribute regular expression. * * @phpstan-return non-falsy-string + * @phpstan-pure */ function get_shortcode_atts_regex() { return '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|\'([^\']*)\'(?:\s|$)|(\S+)(?:\s|$)/'; From 515099f6496ae8681173d9aa115ee688a6f37bd6 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 8 Oct 2026 09:12:05 +0000 Subject: [PATCH 2/4] Docs: Mark more functions as `@phpstan-pure`. Add `@phpstan-pure` to 55 more functions whose result depends only on their arguments and which core calls directly, such as `absint()`, `wp_basename()`, `_wp_array_get()`, `wp_array_slice_assoc()`, `_n_noop()` and `wp_validate_boolean()`. The candidates were found by marking every documented function as pure and removing the tag wherever PHPStan reported a side effect, until none remained. The result was then filtered further to drop any function that reads globals or object state, calls methods or hooks, depends on time or randomness, or performs I/O, directly or through another WordPress function. Callback-only functions, pluggable and drop-in replaceable functions, deprecated functions, polyfills and generated block files are left out. See #65817. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T3XYNUvB9sQNDXA14uJQeH --- src/wp-admin/includes/image-edit.php | 2 ++ src/wp-admin/includes/image.php | 2 ++ src/wp-admin/includes/misc.php | 2 ++ src/wp-admin/includes/upgrade.php | 2 ++ src/wp-includes/abilities-api.php | 2 ++ src/wp-includes/block-editor.php | 2 ++ src/wp-includes/block-patterns.php | 2 ++ src/wp-includes/block-supports/dimensions.php | 2 ++ src/wp-includes/block-supports/elements.php | 2 ++ src/wp-includes/block-supports/layout.php | 6 ++++++ src/wp-includes/block-supports/states.php | 8 ++++++++ src/wp-includes/category-template.php | 2 ++ src/wp-includes/connectors.php | 4 ++++ src/wp-includes/formatting.php | 9 +++++++++ src/wp-includes/functions.php | 10 ++++++++++ src/wp-includes/general-template.php | 2 ++ src/wp-includes/http.php | 2 ++ src/wp-includes/json-schema.php | 2 ++ src/wp-includes/kses.php | 6 ++++++ src/wp-includes/l10n.php | 4 ++++ src/wp-includes/load.php | 1 + src/wp-includes/media.php | 6 ++++++ src/wp-includes/nav-menu.php | 2 ++ src/wp-includes/plugin.php | 2 ++ src/wp-includes/rest-api.php | 8 ++++++++ src/wp-includes/script-loader.php | 4 ++++ src/wp-includes/shortcodes.php | 2 ++ src/wp-includes/user.php | 2 ++ src/wp-includes/view-config.php | 2 ++ 29 files changed, 102 insertions(+) diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php index 2f6bc25740e2d..1f3ac1a3d088a 100644 --- a/src/wp-admin/includes/image-edit.php +++ b/src/wp-admin/includes/image-edit.php @@ -525,6 +525,8 @@ function wp_save_image_file( $filename, $image, $mime_type, $post_id ) { * @param int $w Image width in pixels. * @param int $h Image height in pixels. * @return float|int Image preview ratio. + * + * @phpstan-pure */ function _image_get_preview_ratio( $w, $h ) { $max = max( $w, $h ); diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index a05531c6d9557..d1cf2ca58d048 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -757,6 +757,8 @@ function wp_generate_attachment_metadata( $attachment_id, $file ) { * * @param string $str Fraction string. * @return int|float Returns calculated fraction or integer 0 on invalid input. + * + * @phpstan-pure */ function wp_exif_frac2dec( $str ) { if ( ! is_scalar( $str ) || is_bool( $str ) ) { diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index c1ee93a2849e5..1d3bc8700728b 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -363,6 +363,8 @@ function update_recently_edited( $file ) { * * @param array $allowed_files List of theme file paths. * @return array Tree structure for listing theme files. + * + * @phpstan-pure */ function wp_make_theme_file_tree( $allowed_files ) { $tree_list = array(); diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index 461ab78a219e5..8c97eaa5212e4 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -3640,6 +3640,8 @@ function make_site_theme() { * * @param int $level User level. * @return string User role name. + * + * @phpstan-pure */ function translate_level_to_role( $level ) { switch ( $level ) { diff --git a/src/wp-includes/abilities-api.php b/src/wp-includes/abilities-api.php index abd3684273a5a..31753160f1498 100644 --- a/src/wp-includes/abilities-api.php +++ b/src/wp-includes/abilities-api.php @@ -592,6 +592,8 @@ function wp_get_abilities( array $args = array() ): array { * @param array $meta The ability's meta array. * @param array $conditions The required key/value conditions to match against. * @return bool True if all conditions match, false otherwise. + * + * @phpstan-pure */ function _wp_get_abilities_match_meta( array $meta, array $conditions ): bool { foreach ( $conditions as $key => $value ) { diff --git a/src/wp-includes/block-editor.php b/src/wp-includes/block-editor.php index 830e67cfce013..c244a789947bd 100644 --- a/src/wp-includes/block-editor.php +++ b/src/wp-includes/block-editor.php @@ -397,6 +397,8 @@ function _wp_get_iframed_editor_assets() { * @param array $blocks Array of blocks. * @param string $block_name Name of the block to find. * @return array Found block, or empty array if none found. + * + * @phpstan-pure */ function wp_get_first_block( $blocks, $block_name ) { foreach ( $blocks as $block ) { diff --git a/src/wp-includes/block-patterns.php b/src/wp-includes/block-patterns.php index 3e50b349d242c..5bc5fb60c1bfd 100644 --- a/src/wp-includes/block-patterns.php +++ b/src/wp-includes/block-patterns.php @@ -252,6 +252,8 @@ function _register_core_block_patterns_and_categories() { * * @param array $pattern Pattern as returned from the Pattern Directory API. * @return array Normalized pattern. + * + * @phpstan-pure */ function wp_normalize_remote_block_pattern( $pattern ) { if ( isset( $pattern['block_types'] ) ) { diff --git a/src/wp-includes/block-supports/dimensions.php b/src/wp-includes/block-supports/dimensions.php index 584574f4153ce..bc9f1f977924b 100644 --- a/src/wp-includes/block-supports/dimensions.php +++ b/src/wp-includes/block-supports/dimensions.php @@ -94,6 +94,8 @@ function wp_apply_dimensions_support( $block_type, $block_attributes ) { * * @param mixed $aspect_ratio Aspect-ratio value. * @return bool Whether the value is an explicit aspect ratio. + * + * @phpstan-pure */ function wp_is_explicit_aspect_ratio_value( $aspect_ratio ) { if ( ! is_string( $aspect_ratio ) && ! is_numeric( $aspect_ratio ) ) { diff --git a/src/wp-includes/block-supports/elements.php b/src/wp-includes/block-supports/elements.php index e8076cc1bd3ba..cb2cd64ae2a52 100644 --- a/src/wp-includes/block-supports/elements.php +++ b/src/wp-includes/block-supports/elements.php @@ -29,6 +29,8 @@ function wp_get_elements_class_name(): string { * @param array $block Block object. * @param array $options Per element type options e.g. whether to skip serialization. * @return bool Whether the block needs an elements class name. + * + * @phpstan-pure */ function wp_should_add_elements_class_name( $block, $options ) { if ( ! isset( $block['attrs']['style']['elements'] ) ) { diff --git a/src/wp-includes/block-supports/layout.php b/src/wp-includes/block-supports/layout.php index 0162ae8d5a047..24c5197626828 100644 --- a/src/wp-includes/block-supports/layout.php +++ b/src/wp-includes/block-supports/layout.php @@ -44,6 +44,8 @@ function wp_get_block_style_variation_name_from_registered_style( string $class_ * * @param mixed $layout Layout object. * @return array Child layout values, or an empty array. + * + * @phpstan-pure */ function wp_get_layout_child_values( $layout ) { if ( ! is_array( $layout ) ) { @@ -63,6 +65,8 @@ function wp_get_layout_child_values( $layout ) { * * @param mixed $layout Layout object. * @return array Container layout values, or an empty array. + * + * @phpstan-pure */ function wp_get_layout_container_values( $layout ) { if ( ! is_array( $layout ) ) { @@ -300,6 +304,8 @@ function wp_get_child_layout_style_rules( $selector, $child_layout, $parent_layo * @access private * * @return array[] Layout definitions. + * + * @phpstan-pure */ function wp_get_layout_definitions() { $layout_definitions = array( diff --git a/src/wp-includes/block-supports/states.php b/src/wp-includes/block-supports/states.php index 147f770855c21..89a3a2d41d6c7 100644 --- a/src/wp-includes/block-supports/states.php +++ b/src/wp-includes/block-supports/states.php @@ -78,6 +78,8 @@ function wp_normalize_state_style_for_css_output( $style ) { * * @param array $declarations CSS declarations generated by the style engine. * @return array CSS declarations with fallback border styles applied where needed. + * + * @phpstan-pure */ function wp_get_state_declarations_with_fallback_border_styles( $declarations ) { if ( ! is_array( $declarations ) ) { @@ -123,6 +125,8 @@ function wp_get_state_declarations_with_fallback_border_styles( $declarations ) * * @param array $declarations CSS declarations generated by the style engine. * @return array CSS declarations with background resets applied where needed. + * + * @phpstan-pure */ function wp_get_state_declarations_with_background_resets( $declarations ) { if ( ! is_array( $declarations ) ) { @@ -154,6 +158,8 @@ function wp_get_state_declarations_with_background_resets( $declarations ) { * * @param array $state_style State style object. * @return array State style object with fallback dimension styles applied where needed. + * + * @phpstan-pure */ function wp_get_state_style_with_fallback_dimension_styles( $state_style ) { if ( ! is_array( $state_style ) ) { @@ -295,6 +301,8 @@ function wp_get_state_style_groups( $state_style, $block_selectors ) { * @param array $state_style State style object. * @param array $nested_keys Keys to remove from the root style object. * @return array Root-only style object. + * + * @phpstan-pure */ function wp_get_root_state_style( $state_style, $nested_keys ) { if ( ! is_array( $state_style ) ) { diff --git a/src/wp-includes/category-template.php b/src/wp-includes/category-template.php index dbb1ff8ab0d2c..214a735ac6c37 100644 --- a/src/wp-includes/category-template.php +++ b/src/wp-includes/category-template.php @@ -803,6 +803,8 @@ function wp_tag_cloud( $args = '' ) { * * @param int $count Number of posts with that tag. * @return int Scaled count. + * + * @phpstan-pure */ function default_topic_count_scale( $count ) { return (int) round( log10( $count + 1 ) * 100 ); diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 9c83a973eba43..f4210ae04d6b5 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -418,6 +418,8 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re * * @param string $key The API key to mask. * @return string The masked key, e.g. "************fj39". + * + * @phpstan-pure */ function _wp_connectors_mask_api_key( string $key ): string { if ( strlen( $key ) <= 4 ) { @@ -480,6 +482,8 @@ function _wp_connectors_get_api_key_source( string $setting_name, string $env_va * @param string $value The raw credentials string. * @return array{username: string, password: string} Parsed credentials. Both values * are empty when the string is malformed. + * + * @phpstan-pure */ function wp_connectors_parse_application_password_credentials( string $value ): array { $separator = strpos( $value, ':' ); diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 3e01bfc4de679..3b74662abb107 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -2834,6 +2834,7 @@ function format_to_edit( $content, $rich_text = false ) { * @param int $threshold Digit places number needs to be to not have zeros added. * @return string Adds leading zeros to number if needed. * + * @phpstan-pure * @phpstan-return ( * $threshold is 0 * ? lowercase-string&non-empty-string&numeric-string @@ -3311,6 +3312,8 @@ function make_clickable( $text ) { * @param string $text The string to split. * @param int $goal The desired chunk length. * @return array Numeric array of chunks. + * + * @phpstan-pure */ function _split_str_by_whitespace( $text, $goal ) { $chunks = array(); @@ -5844,6 +5847,8 @@ function _sanitize_text_fields( $str, $keep_newlines = false ) { * @param string $path A path. * @param string $suffix If the filename ends in suffix this will also be cut off. * @return string The base name of the given path. + * + * @phpstan-pure */ function wp_basename( $path, $suffix = '' ) { return urldecode( basename( str_replace( array( '%2F', '%5C' ), '/', urlencode( $path ) ), $suffix ) ); @@ -6381,6 +6386,8 @@ function wp_staticize_emoji_for_email( $mail ) { * * @param string $type Optional. Which array type to return. Accepts 'partials' or 'entities', default 'entities'. * @return array An array to match all emoji that WordPress recognises. + * + * @phpstan-pure */ function _wp_emoji_list( $type = 'entities' ) { // Do not remove the START/END comments - they're used to find where to insert the arrays. @@ -6406,6 +6413,8 @@ function _wp_emoji_list( $type = 'entities' ) { * @param string $url URL to shorten. * @param int $length Optional. Maximum length of the shortened URL. Default 35 characters. * @return string Shortened URL. + * + * @phpstan-pure */ function url_shorten( $url, $length = 35 ) { $stripped = str_replace( array( 'https://', 'http://', 'www.' ), '', $url ); diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 5169927abd9ff..a4e06bafff3d5 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -1313,6 +1313,7 @@ function wp_removable_query_args() { * @param array $input_array Array to walk while sanitizing contents. * @return array Sanitized $input_array. * + * @phpstan-pure * @phpstan-template T of array * @phpstan-param T $input_array * @phpstan-return array, ( value-of is string ? string : value-of )> @@ -5166,6 +5167,7 @@ function wp_parse_slug_list( $input_list ): array { * @param string[] $keys The list of keys. * @return array The array slice. * + * @phpstan-pure * @phpstan-template TKey of string * @phpstan-template TValue * @phpstan-param array $input_array @@ -5231,6 +5233,8 @@ function wp_recursive_ksort( &$input_array ) { * @param mixed $default_value Optional. The return value if the path does not exist within the array, * or if `$input_array` or `$path` are not arrays. Default null. * @return mixed The value from the path specified. + * + * @phpstan-pure */ function _wp_array_get( $input_array, $path, $default_value = null ) { // Confirm $path is valid. @@ -7928,6 +7932,8 @@ function reset_mbstring_encoding() { * * @param mixed $value Boolean value to validate. * @return bool Whether the value is validated. + * + * @phpstan-pure */ function wp_validate_boolean( $value ) { if ( is_bool( $value ) ) { @@ -9297,6 +9303,8 @@ function is_php_version_compatible( $required ) { * @param int|float $actual The actual number. * @param int|float $precision Optional. The allowed variation. Default 1. * @return bool Whether the numbers match within the specified precision. + * + * @phpstan-pure */ function wp_fuzzy_number_match( $expected, $actual, $precision = 1 ) { return abs( (float) $expected - (float) $actual ) <= $precision; @@ -9461,6 +9469,8 @@ function wp_admin_notice( $message, $args = array() ) { * * @param string $mime_type The mime type to check. * @return bool Whether the mime type is for a HEIC/HEIF image. + * + * @phpstan-pure */ function wp_is_heic_image_mime_type( $mime_type ) { $heic_mime_types = array( diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 73178b9eca23b..2b0860cc44bf2 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -2514,6 +2514,8 @@ function wp_get_archives( $args = '' ) { * * @param int $num Number of day. * @return float Days since the start of the week. + * + * @phpstan-pure */ function calendar_week_mod( $num ) { $base = 7; diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index 1a189a261306b..55ba274bec015 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -807,6 +807,7 @@ function wp_parse_url( $url, $component = -1 ) { * doesn't exist in the given URL; a string or - in the case of * PHP_URL_PORT - integer when it does. See parse_url()'s return values. * + * @phpstan-pure * @phpstan-param false|array{ * scheme?: string, * host?: string, @@ -863,6 +864,7 @@ function _get_component_from_parsed_url_array( $url_parts, $component = -1 ) { * @param int $constant PHP_URL_* constant. * @return string|false The named key or false. * + * @phpstan-pure * @phpstan-param int<-1, 7> $constant * @phpstan-return 'scheme'|'host'|'port'|'user'|'pass'|'path'|'query'|'fragment'|false */ diff --git a/src/wp-includes/json-schema.php b/src/wp-includes/json-schema.php index 782ad9714fe36..010fc1f89ce31 100644 --- a/src/wp-includes/json-schema.php +++ b/src/wp-includes/json-schema.php @@ -102,6 +102,8 @@ function wp_prepare_json_schema_for_client( array $schema, string $schema_profil * @param array $schema The schema array. * @param array $allowed_keywords Lookup map of allowed JSON Schema keywords. * @return array The prepared schema. + * + * @phpstan-pure */ function _wp_prepare_json_schema_for_client_with_allowed_keywords( array $schema, array $allowed_keywords ): array { if ( isset( $schema['type'] ) && 'object' === $schema['type'] && isset( $schema['default'] ) ) { diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 0a7ea7ff3087e..aea39aa70c094 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2313,6 +2313,8 @@ function wp_kses_hook( $content, $allowed_html, $allowed_protocols ) { * @since 1.0.0 * * @return string KSES version number. + * + * @phpstan-pure */ function wp_kses_version() { return '0.2.2'; @@ -3113,6 +3115,8 @@ function wp_kses_stripslashes( $content ) { * * @param array $inarray Unfiltered array. * @return array Fixed array with all lowercase keys. + * + * @phpstan-pure */ function wp_kses_array_lc( $inarray ) { $outarray = array(); @@ -3388,6 +3392,8 @@ function wp_kses_normalize_entities3( $matches ) { * * @param int $i Unicode codepoint. * @return bool Whether or not the codepoint is a valid Unicode codepoint. + * + * @phpstan-pure */ function valid_unicode( $i ) { $i = (int) $i; diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index e90f97fd2025a..15686b83c9041 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -262,6 +262,8 @@ function translate( $text, $domain = 'default' ) { * * @param string $text A pipe-delimited string. * @return string Either $text or everything before the last pipe. + * + * @phpstan-pure */ function before_last_bar( $text ) { $last_bar = strrpos( $text, '|' ); @@ -678,6 +680,7 @@ function _nx( $single, $plural, $number, $context, $domain = 'default' ) { * @type string|null $domain Text domain. * } * + * @phpstan-pure * @phpstan-param literal-string $singular * @phpstan-param literal-string $plural * @phpstan-param literal-string|null $domain @@ -736,6 +739,7 @@ function _n_noop( $singular, $plural, $domain = null ) { * @type string|null $domain Text domain. * } * + * @phpstan-pure * @phpstan-param literal-string $singular * @phpstan-param literal-string $plural * @phpstan-param literal-string $context diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 1558a44189226..3723410e73996 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -1469,6 +1469,7 @@ function is_multisite() { * * @param mixed $maybeint Data you wish to have converted to a non-negative integer. * @return int A non-negative integer. + * @phpstan-pure * @phpstan-return non-negative-int */ function absint( $maybeint ): int { diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 2ffa92f2edba0..757ee9356d41b 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -164,6 +164,8 @@ function image_constrain_size_for_editor( $width, $height, $size = 'medium', $co * @param int|string $width Image width in pixels. * @param int|string $height Image height in pixels. * @return string HTML attributes for width and, or height. + * + * @phpstan-pure */ function image_hwstring( $width, $height ) { $out = ''; @@ -1374,6 +1376,8 @@ function _wp_get_attachment_relative_path( $file ) { * @type int $0 Image width. * @type int $1 Image height. * } + * + * @phpstan-pure */ function _wp_get_image_size_from_meta( $size_name, $image_meta ) { if ( 'full' === $size_name ) { @@ -2175,6 +2179,8 @@ function wp_img_tag_add_auto_sizes( string $image ): string { * * @param string $sizes_attr The 'sizes' attribute value. * @return bool True if the 'auto' keyword is present, false otherwise. + * + * @phpstan-pure */ function wp_sizes_attribute_includes_valid_auto( string $sizes_attr ): bool { list( $first_size ) = explode( ',', $sizes_attr, 2 ); diff --git a/src/wp-includes/nav-menu.php b/src/wp-includes/nav-menu.php index ed49892ac0eb6..920b2511956b3 100644 --- a/src/wp-includes/nav-menu.php +++ b/src/wp-includes/nav-menu.php @@ -1330,6 +1330,8 @@ function wp_map_nav_menu_locations( $new_nav_menu_locations, $old_nav_menu_locat * * @param array $menu_item_data The menu item data array. * @return array The menu item data with reset menu_item_parent. + * + * @phpstan-pure */ function _wp_reset_invalid_menu_item_parent( $menu_item_data ) { if ( ! is_array( $menu_item_data ) ) { diff --git a/src/wp-includes/plugin.php b/src/wp-includes/plugin.php index a3e9afacdb8dc..900905874008c 100644 --- a/src/wp-includes/plugin.php +++ b/src/wp-includes/plugin.php @@ -862,6 +862,8 @@ function wp_register_plugin_realpath( $file ) { * * @param string $file The filename of the plugin (__FILE__). * @return string the filesystem path of the directory that contains the plugin. + * + * @phpstan-pure */ function plugin_dir_path( $file ) { return trailingslashit( dirname( $file ) ); diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index dbfbbeaeb576d..6f0448cbfe3ff 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -927,6 +927,8 @@ function rest_send_allow_header( $response, $server, $request ) { * @param array $array2 An array to compare keys against. * @return array An associative array containing all the entries of array1 which have keys * that are present in all arguments. + * + * @phpstan-pure */ function _rest_array_intersect_key_recursive( $array1, $array2 ) { $array1 = array_intersect_key( $array1, $array2 ); @@ -1561,6 +1563,8 @@ function rest_sanitize_boolean( $value ) { * * @param bool|string $maybe_bool The value being evaluated. * @return bool True if a boolean, otherwise false. + * + * @phpstan-pure */ function rest_is_boolean( $maybe_bool ) { if ( is_bool( $maybe_bool ) ) { @@ -2174,6 +2178,8 @@ function rest_validate_enum( $value, $args, $param ) { * @since 5.6.0 * * @return string[] All valid JSON schema properties. + * + * @phpstan-pure */ function rest_get_allowed_schema_keywords() { return array( @@ -3197,6 +3203,8 @@ function rest_filter_response_by_context( $response_data, $schema, $context ) { * * @param array $schema The schema to modify. * @return array The modified schema. + * + * @phpstan-pure */ function rest_default_additional_properties_to_false( $schema ) { $type = (array) $schema['type']; diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 2d839af4a1a82..c4287237441b0 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -4655,6 +4655,8 @@ public function replace( string $text ): void { * * @param string $html_attribute_name Raw attribute name as found in the source HTML. * @return string|null Transformed `dataset` name, if interpretable as a custom data attribute, else `null`. + * + * @phpstan-pure */ function wp_js_dataset_name( string $html_attribute_name ): ?string { if ( 0 !== substr_compare( $html_attribute_name, 'data-', 0, 5, true ) ) { @@ -4734,6 +4736,8 @@ function wp_js_dataset_name( string $html_attribute_name ): ?string { * @param string $js_dataset_name Name of JS `dataset` property to transform. * @return string|null Corresponding name of an HTML custom data attribute for the given dataset name, * if possible to represent in HTML, otherwise `null`. + * + * @phpstan-pure */ function wp_html_custom_data_attribute_name( string $js_dataset_name ): ?string { $end = strlen( $js_dataset_name ); diff --git a/src/wp-includes/shortcodes.php b/src/wp-includes/shortcodes.php index 3ee3bd771072b..2853fe8a6ea14 100644 --- a/src/wp-includes/shortcodes.php +++ b/src/wp-includes/shortcodes.php @@ -576,6 +576,8 @@ function do_shortcodes_in_html_tags( $content, $ignore_html, $tagnames ) { * * @param string $content Content to search for placeholders. * @return string Content with placeholders removed. + * + * @phpstan-pure */ function unescape_invalid_shortcodes( $content ) { // Clean up entire string, avoids re-parsing HTML. diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 1d0523cfc9a9a..0f2aae3639014 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -4043,6 +4043,8 @@ function new_user_email_admin_notice() { * @access private * * @return string[] List of core privacy action types. + * + * @phpstan-pure */ function _wp_privacy_action_request_types() { return array( diff --git a/src/wp-includes/view-config.php b/src/wp-includes/view-config.php index 8765a5d095008..9c9ed6157bd22 100644 --- a/src/wp-includes/view-config.php +++ b/src/wp-includes/view-config.php @@ -26,6 +26,8 @@ * @param string $kind The entity kind (e.g. `postType`). * @param string $name The entity name (e.g. `page`). * @return string The filter name. + * + * @phpstan-pure */ function wp_get_entity_view_config_hook_name( $kind, $name ) { return strtolower( "get_entity_view_config_{$kind}_{$name}" ); From a695177872fe8a782ee9ce37d3a661f49009004d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 8 Oct 2026 20:48:09 +0000 Subject: [PATCH 3/4] Docs: Mark more functions as `@phpstan-pure`. Add `@phpstan-pure` to `wp_parse_list()`, `wp_parse_id_list()`, `is_php_version_compatible()`, `_get_wptexturize_shortcode_regex()`, `remove_serialized_parent_block()`, `extract_serialized_parent_block()`, `get_query_pagination_arrow()` and `get_comments_pagination_arrow()`. These were missed before because the filter used to pick candidates also rejected property reads on parameters, `array_map()` and `array_filter()` with pure callbacks, and `version_compare()`. See #65817. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T3XYNUvB9sQNDXA14uJQeH --- src/wp-includes/blocks.php | 6 ++++++ src/wp-includes/formatting.php | 2 ++ src/wp-includes/functions.php | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 1701548d8d3fa..50a52e97436cd 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1391,6 +1391,8 @@ function apply_block_hooks_to_content_from_post_object( * * @param string $serialized_block The serialized markup of a block and its inner blocks. * @return string The serialized markup of the inner blocks. + * + * @phpstan-pure */ function remove_serialized_parent_block( $serialized_block ) { $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); @@ -1408,6 +1410,8 @@ function remove_serialized_parent_block( $serialized_block ) { * * @param string $serialized_block The serialized markup of a block and its inner blocks. * @return string The serialized markup of the wrapper block. + * + * @phpstan-pure */ function extract_serialized_parent_block( $serialized_block ) { $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); @@ -3086,6 +3090,7 @@ static function ( $format ) { * @param bool $is_next Flag for handling `next/previous` blocks. * @return string|null The pagination arrow HTML or null if there is none. * + * @phpstan-pure * @phpstan-return non-falsy-string|null */ function get_query_pagination_arrow( $block, $is_next ) { @@ -3188,6 +3193,7 @@ function build_comment_query_vars_from_block( $block ) { * Accepts 'next' or 'previous'. Default 'next'. * @return string|null The pagination arrow HTML or null if there is none. * + * @phpstan-pure * @phpstan-return non-falsy-string|null */ function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 3b74662abb107..978e3f5cc52f0 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -727,6 +727,8 @@ function _get_wptexturize_split_regex( $shortcode_regex = '' ) { * * @param string[] $tagnames Array of shortcodes to find. * @return string The regular expression. + * + * @phpstan-pure */ function _get_wptexturize_shortcode_regex( $tagnames ) { $tagregexp = implode( '|', array_map( 'preg_quote', $tagnames ) ); diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index a4e06bafff3d5..b9f77c49739f6 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5089,6 +5089,7 @@ function wp_parse_args( $args, $defaults = array() ): array { * @param mixed[]|string|int $input_list List of values. * @return array Array of scalar values. A string is split into a list, while an array * keeps its keys, so the result is not necessarily a list. + * @phpstan-pure * @phpstan-return ( * $input_list is string|int ? list : ( * $input_list is array ? array : array @@ -5120,6 +5121,7 @@ function wp_parse_list( $input_list ): array { * @return int[] Sanitized array of IDs. May include zero. Keys are preserved * from the input and `array_unique()` may leave gaps, so the * result is not necessarily a list. + * @phpstan-pure * @phpstan-return array */ function wp_parse_id_list( $input_list ): array { @@ -9287,6 +9289,8 @@ function is_wp_version_compatible( $required ) { * * @param string $required Minimum required PHP version. * @return bool True if required version is compatible or empty, false if not. + * + * @phpstan-pure */ function is_php_version_compatible( $required ) { return empty( $required ) || version_compare( PHP_VERSION, $required, '>=' ); From c6b5b731e79c0938572f10409b45849d93da6b2a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 9 Oct 2026 07:51:41 +0000 Subject: [PATCH 4/4] Docs: Mark more functions as `@phpstan-pure`. Add `@phpstan-pure` to `make_before_block_visitor()`, `make_after_block_visitor()` and `wp_caption_input_textarea()`. Creating and returning a closure has no side effects; those only happen when the closure is called, and PHPStan already treats calling the returned `callable` as possibly impure. See #65817. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T3XYNUvB9sQNDXA14uJQeH --- src/wp-admin/includes/media.php | 1 + src/wp-includes/blocks.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 42de0269a05c2..d48eb7f53431c 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -1351,6 +1351,7 @@ function image_link_input_fields( $post, $url_type = '' ) { * @param WP_Post $edit_post Attachment WP_Post object. * @return string HTML markup for the textarea element. * + * @phpstan-pure * @phpstan-return non-falsy-string */ function wp_caption_input_textarea( $edit_post ) { diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 50a52e97436cd..585d26b8a3bea 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1601,6 +1601,8 @@ function insert_hooked_blocks_into_rest_response( $response, $post ) { * Default: 'insert_hooked_blocks'. * @return callable A function that returns the serialized markup for the given block, * including the markup for any hooked blocks before it. + * + * @phpstan-pure */ function make_before_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { /** @@ -1658,6 +1660,8 @@ function make_before_block_visitor( $hooked_blocks, $context, $callback = 'inser * Default: 'insert_hooked_blocks'. * @return callable A function that returns the serialized markup for the given block, * including the markup for any hooked blocks after it. + * + * @phpstan-pure */ function make_after_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { /**