Skip to content

Update PHPStan 2.2.13 - #13410

Closed
staabm wants to merge 3 commits into
WordPress:trunkfrom
staabm:upstan
Closed

Update PHPStan 2.2.13#13410
staabm wants to merge 3 commits into
WordPress:trunkfrom
staabm:upstan

Conversation

@staabm

@staabm staabm commented Sep 5, 2026

Copy link
Copy Markdown

at PHPStan we worked hard to improve performance on recent releases. Updating the tooling makes sure Wordpress can use latest additions and get the best possible PHPStan experience

before this PR (PHPStan 2.2.5)

time composer phpstan
197.13s user 10.28s system 667% cpu 31.084 total

after this PR (PHPStan 2.2.13)

time composer phpstan
112.30s user 5.09s system 981% cpu 11.964 total

running on Macbook M4-Pro, PHP 8.5.10 (cli)

=> ~20 seconds faster (wall-time; cold result-cache)

https://core.trac.wordpress.org/ticket/66051

@github-actions

github-actions Bot commented Sep 5, 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. You can also manually ask me to refresh this list by adding the props-bot label.

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

Props markusstaab, westonruter.

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

$safe_text = (string) preg_replace_callback(
$regex,
static function ( $matches ) {
if ( ! isset( $matches[0] ) ) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixes

  4851   Call to function array_key_exists() with 0 and array{0: string, non_cdata_followed_by_cdata: '', 1: '', 2: '', cdata: '', 3: '', 4: '', non_cdata: string, ...}|array{0:  
         string, non_cdata_followed_by_cdata: string, 1: string, 2: string, cdata: non-falsy-string, 3: non-falsy-string, 4: non-falsy-string} will always evaluate to true.       
         🪪  function.alreadyNarrowedType                                                                                                                                          
         at src/wp-includes/formatting.php:4851                                                                                                                                    

@github-actions

github-actions Bot commented Sep 5, 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.

@staabm

staabm commented Sep 5, 2026

Copy link
Copy Markdown
Author

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

done

count: 2
path: ../../../src/wp-includes/class-wp-embed.php
-
message: '#^Parameter \#1 \$handle of function curl_getinfo expects resource, \(resource\|false\) given\.$#'

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Will be fixed upstream with phpstan/phpstan#15185

@westonruter

westonruter commented Sep 6, 2026

Copy link
Copy Markdown
Member

🤖 Comment by Claude Fable 5.1

Updated after 09218f6, which corrects the _wp_die_process_input() docblock; see the functions.php entries below.

The baseline changes were analyzed to understand why each error appeared or disappeared between PHPStan 2.2.5 and 2.2.13. Both versions were run on the affected files with the baselines stripped, the output was diffed, each difference was bisected across 2.2.6 through 2.2.12, and each was reduced to a standalone repro.

Summary

Baseline change Release Cause Which side is right?
Removed: if.alwaysFalse in load.php 2.2.6 Negative function_exists() narrowing is now forgotten after require/include New (the old report was a false positive)
Added: 6th WP_Theme::$version in class-wp-debug-data.php 2.2.6 2.2.5 bug: a remembered narrowing on a variable hid a later fetch of the same expression New
Removed: is_wp_error() always true in ajax-actions.php 2.2.7 Impossible-check rules now honor treatPhpDocTypesAsCertain: false New
Removed: is_string() always true in block-editor.php 2.2.7 Same New
Added: 3 × is_scalar() in functions.php (was 3 × is_scalar() + 1 × is_string() before 09218f6) 2.2.7 list() destructuring makes a phpdoc-only type look native; the _wp_die_process_input() docblock was also incomplete New, now that the docblock is fixed
Removed: empty($title) in plugin.php 2.2.8 Same phpdoc-certainty fix, for empty() New
Code change: esc_xml() guard 2.2.8 isset() inside callbacks now reaches the impossible-check rules New
Added: curl_getinfo()/finfo_file() argument.type 2.2.10 Two functionMap entries were deleted, so stub signatures with a native resource parameter now apply Inconsistent rather than wrong

Removed errors

load.php line 863. The check is function_exists( 'wp_cache_init' ) right after require_once of the object-cache drop-in, inside a block guarded by the negated check. 2.2.5 carried the negative narrowing across the require and reported the inner check as always false. 2.2.6 forgets function_exists()/class_exists() narrowings when global state may change (phpstan/phpstan-src#5998) and invalidates volatile expressions on include (phpstan/phpstan-src#6009). The repro confirms the report vanishes only when a require sits between the two checks.

The three phpdoc-only checks. Core sets treatPhpDocTypesAsCertain: false, which is supposed to suppress "always true" reports that rest solely on docblock types. All three qualify: the term update result is typed only by the wp_update_term() docblock, the preload paths come from the hook docblock via the apply_filters() return type extension, and $title is a @global string. 2.2.5 reported them anyway. With the setting flipped to true, all three come back under 2.2.13, which confirms the mechanism. The 2.2.7 and 2.2.8 release notes describe the rework of the constant-condition and impossible-check machinery that fixed this.

Added errors

The sixth WP_Theme::$version report. $theme is WP_Theme at both sites under both versions, so this is not a type change. A structural bisect showed 2.2.5 hid the report only when both the earlier read into $theme_version and the ! empty( $theme_version ) checks were present. Minimal repro:

$v = $t->version;
if ( ! empty( $v ) ) { }
$w = $t->version; // silent in 2.2.5, reported in 2.2.13; reported in both without the empty() check

Narrowing the variable also narrowed the remembered expression, and the later fetch used that entry without reaching the undefined-property rule. This is not called out in the 2.2.6 notes; it fell out of the expression-handling fixes.

The wp_die() handler checks. Each follows list( $message, ... ) = _wp_die_process_input(), whose hash-notation return becomes an array shape through core's HashNotationVisitor. Two things were going on here:

  1. The docblock was wrong. wp_die() documents string|WP_Error|int for the message, since the legacy Ajax handlers use -1/0/1, and the helper passes anything that is not a WP_Error through untouched. Its docblock only admitted string|WP_Error, so the processed message was typed as a plain string, and the is_string() guard in _default_wp_die_handler() that handles the integer case looked unreachable. Commit 09218f6 documents the parameter as string|WP_Error|int and the first element of the hash as string|int. That is_string() entry is gone from the baseline. The three is_scalar() entries remain, now reading "with int|string", and those are correct: int|string is always scalar, so under the documented contract the guards are dead code, useful only against callers passing an array or object the docblock does not permit.

  2. PHPStan treats list() differently from a direct assignment. Under treatPhpDocTypesAsCertain: false a direct $message = $result[0] from the same helper is not reported, but list( $message ) = ... is, in every version from 2.2.7 on. That is the opposite direction from the certainty fix above and still looks like an upstream inconsistency worth filing. It no longer matters for core, though: with the docblock corrected, the remaining reports are legitimate whichever way PHPStan resolves it.

curl_getinfo() and finfo_file(). In 2.2.10, phpstan/phpstan-src#6243 deleted exactly these two functions from PHPStan's functionMap. Their signatures now come from the PhpStorm stubs, whose version-aware parameter resolves to a native resource on the PHP 7.4 floor, and that rejects the false half of what curl_init() and finfo_open() return. The functionMap-sourced resource parameter accepted the same union in both versions, even with treatPhpDocTypesAsCertain: true. That is why the sibling calls on the same handle, such as curl_exec(), curl_setopt() and finfo_close(), stay silent: they still have functionMap entries. So the reporting is inconsistent, but the underlying observation is real, since core never checks either function for failure. The benevolent-union argument in phpstan/phpstan#15185 would not cover finfo_open(), whose return type is a plain resource|false rather than a benevolent union.

esc_xml(). The $matches shape is identical in 2.2.5, so what changed in 2.2.8 is that isset() inside a callback closure now reaches the rules, via the new isset descriptors (phpstan/phpstan-src#6144) and running the impossible-check rules on post-processing virtual nodes (phpstan/phpstan-src#6157). Removing the guard is correct; offset 0 always exists in a preg_replace_callback() callback.

wp_die() accepts an integer message, which the legacy Ajax handlers use for
their -1/0/1 responses, and _wp_die_process_input() passes it through
untouched since only a WP_Error is rewritten. Its docblock only admitted
string|WP_Error, so the processed message was typed as a plain string and
PHPStan 2.2.13 reported the is_string() and is_scalar() guards in the
wp_die() handlers as always true.

Type the parameter as string|WP_Error|int and the first element of the
returned hash as string|int. The is_string() check in
_default_wp_die_handler() is now reachable and leaves the baseline; the
three is_scalar() entries remain, with their message updated to reflect
the int|string type, since int|string is always scalar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
wporg-sync pushed a commit that referenced this pull request Sep 7, 2026
Recent PHPStan releases bring substantial performance improvements. The baselines are regenerated for the new version.

The docblock of `_wp_die_process_input()` is also corrected to admit the integer message that `wp_die()` accepts for the legacy Ajax responses, which the helper passes through untouched. This removes a baseline entry for a guard in `_default_wp_die_handler()` that the incomplete type made look unreachable.

Developed in #13410.
Follow-up to r44666, r53144, r62703, r62798.

Props markusstaab, westonruter.
See #65817.
Fixes #66051.


git-svn-id: https://develop.svn.wordpress.org/trunk@63510 602fd350-edb4-49c9-b593-d223f7449a82
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 63510
GitHub commit: f799969

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions Bot closed this Sep 7, 2026
wporg-sync pushed a commit to WordPress/WordPress that referenced this pull request Sep 7, 2026
Recent PHPStan releases bring substantial performance improvements. The baselines are regenerated for the new version.

The docblock of `_wp_die_process_input()` is also corrected to admit the integer message that `wp_die()` accepts for the legacy Ajax responses, which the helper passes through untouched. This removes a baseline entry for a guard in `_default_wp_die_handler()` that the incomplete type made look unreachable.

Developed in WordPress/wordpress-develop#13410.
Follow-up to r44666, r53144, r62703, r62798.

Props markusstaab, westonruter.
See #65817.
Fixes #66051.

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


git-svn-id: http://core.svn.wordpress.org/trunk@62686 1a063a9b-81f0-0310-95a4-ce76da25c4cd
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