From 0308f9ee6602ffdde124f271818e61393166c8b5 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 7 Nov 2025 20:44:01 +0200 Subject: [PATCH 01/31] tests: phpstan level 0 --- .github/workflows/php-static-analysis.yml | 100 ++++++++++++++ .../reusable-php-static-analysis.yml | 95 +++++++++++++ .gitignore | 1 + composer.json | 2 + package.json | 1 + phpcs.xml.dist | 3 + phpstan.neon.dist | 35 +++++ .../includes/class-wp-filesystem-ssh2.php | 1 + src/wp-admin/press-this.php | 4 +- src/wp-includes/class-wp-theme-json.php | 4 +- ...-wp-customize-background-image-setting.php | 1 + .../class-wp-customize-filter-setting.php | 1 + ...lass-wp-customize-header-image-setting.php | 1 + src/wp-includes/media.php | 2 +- .../class-wp-style-engine-css-rules-store.php | 1 + src/wp-includes/template.php | 2 +- tests/phpstan/README.md | 84 ++++++++++++ tests/phpstan/base.neon | 125 ++++++++++++++++++ tests/phpstan/baseline.php | 3 + tests/phpstan/bootstrap.php | 95 +++++++++++++ 20 files changed, 556 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/php-static-analysis.yml create mode 100644 .github/workflows/reusable-php-static-analysis.yml create mode 100644 phpstan.neon.dist create mode 100644 tests/phpstan/README.md create mode 100644 tests/phpstan/base.neon create mode 100644 tests/phpstan/baseline.php create mode 100644 tests/phpstan/bootstrap.php diff --git a/.github/workflows/php-static-analysis.yml b/.github/workflows/php-static-analysis.yml new file mode 100644 index 0000000000000..9b15ee6f9f0fe --- /dev/null +++ b/.github/workflows/php-static-analysis.yml @@ -0,0 +1,100 @@ +name: PHPStan Static Analysis + +on: + # PHPStan testing was introduced in @todo. + push: + branches: + - trunk + - '6.9' + - '[7-9].[0-9]' + tags: + - '6.9' + - '6.9.[0-9]+' + - '[7-9].[0-9]' + - '[7-9]+.[0-9].[0-9]+' + pull_request: + branches: + - trunk + - '6.9' + - '[7-9].[0-9]' + paths: + # This workflow only scans PHP files. + - '**.php' + # These files configure Composer. Changes could affect the outcome. + - 'composer.*' + # These files configure PHPStan. Changes could affect the outcome. + - 'phpstan.neon.dist' + - 'tests/phpstan/base.neon' + # Confirm any changes to relevant workflow files. + - '.github/workflows/php-static-analysis.yml' + - '.github/workflows/reusable-php-static-analysis.yml' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs PHPStan Static Analysis. + phpstan: + name: PHP coding standards + uses: ./.github/workflows/reusable-php-static-analysis.yml + permissions: + contents: read + if: ${{ github.repository == 'WordPress/wordpress-develop' || ( github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' ) }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ phpstan ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/reusable-php-static-analysis.yml b/.github/workflows/reusable-php-static-analysis.yml new file mode 100644 index 0000000000000..5e8d592ed2c62 --- /dev/null +++ b/.github/workflows/reusable-php-static-analysis.yml @@ -0,0 +1,95 @@ +## +# A reusable workflow that runs PHP Static Analysis tests. +## +name: PHP Static Analysis + +on: + workflow_call: + inputs: + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs PHP static analysis tests. + # + # Violations are reported inline with annotations. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up PHP. + # - Logs debug information. + # - Installs Composer dependencies. + # - Configures caching for PHP static analysis scans. + # - Make Composer packages available globally. + # - Runs PHPStan static analysis (with Pull Request annotations). + # - Saves the PHPStan result cache. + # - Ensures version-controlled files are not modified or deleted. + phpstan: + name: Run PHP static analysis + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up PHP + uses: shivammathur/setup-php@20529878ed81ef8e78ddf08b480401e6101a850f # v2.35.3 + with: + php-version: ${{ inputs.php-version }} + coverage: none + tools: cs2pr + + - name: Log debug information + run: | + composer --version + + # This date is used to ensure that the Composer cache is cleared at least once every week. + # http://man7.org/linux/man-pages/man1/date.1.html + - name: "Get last Monday's date" + id: get-date + run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # v3.1.1 + with: + custom-cache-suffix: ${{ steps.get-date.outputs.date }} + + - name: Make Composer packages available globally + run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" + + - name: Cache PHP Static Analysis scan cache + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: .cache # This is defined in the base.neon file. + key: "phpstan-result-cache-${{ github.run_id }}" + restore-keys: | + phpstan-result-cache- + + - name: Run PHP static analysis tests + id: phpstan + run: phpstan analyse -vvv --error-format=checkstyle | cs2pr + + - name: "Save result cache" + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + if: ${{ !cancelled() }} + with: + path: .cache + key: "phpstan-result-cache-${{ github.run_id }}" + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.gitignore b/.gitignore index 01314e1a67139..ed8ecebd0f3e5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ wp-tests-config.php /build /tests/phpunit/build /wp-cli.local.yml +/phpstan.neon /jsdoc /composer.lock /vendor diff --git a/composer.json b/composer.json index c636b2e7e680f..07b84ee4add17 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "squizlabs/php_codesniffer": "3.13.2", "wp-coding-standards/wpcs": "~3.2.0", "phpcompatibility/phpcompatibility-wp": "~2.1.3", + "phpstan/phpstan": "~1.12.32", "yoast/phpunit-polyfills": "^1.1.0" }, "config": { @@ -32,6 +33,7 @@ "lock": false }, "scripts": { + "analyse": "@php ./vendor/bin/phpstan analyse --memory-limit=2G", "compat": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcompat.xml.dist --report=summary,source", "format": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf --report=summary,source", "lint": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --report=summary,source", diff --git a/package.json b/package.json index 7e2bab7284f68..b3a46f4f09dd6 100644 --- a/package.json +++ b/package.json @@ -192,6 +192,7 @@ "env:logs": "node ./tools/local-env/scripts/docker.js logs", "env:pull": "node ./tools/local-env/scripts/docker.js pull", "test:performance": "wp-scripts test-playwright --config tests/performance/playwright.config.js", + "test:php:stan": "node ./tools/local-env/scripts/docker.js run --rm php ./vendor/bin/phpstan analyse --memory-limit=2G", "test:php": "node ./tools/local-env/scripts/docker.js run --rm php ./vendor/bin/phpunit", "test:coverage": "npm run test:php -- --coverage-html ./coverage/html/ --coverage-php ./coverage/php/report.php --coverage-text=./coverage/text/report.txt", "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a8387b3604c9b..efb679fb6c13b 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -81,6 +81,9 @@ /tests/phpunit/build* /tests/phpunit/data/* + + /tests/phpstan/* + /tools/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000000..2bffeb442454f --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,35 @@ +# PHPStan configuration for WordPress Core. +# +# To overload this configuration, copy this file to phpstan.neon and adjust as needed. +# +# https://phpstan.org/config-reference + +includes: + # The WordPress Core configuration file includes the base configuration for the WordPress codebase. + - tests/phpstan/base.neon + # The baseline file includes preexisting errors in the codebase that should be ignored. + # https://phpstan.org/user-guide/baseline + - tests/phpstan/baseline.php + +parameters: + # https://phpstan.org/user-guide/rule-levels + level: 0 + reportUnmatchedIgnoredErrors: false + + ignoreErrors: + # Level 0: + - # Inner functions arent supported by PHPstan. + message: '#Function wxr_[a-z_]+ not found#' + path: src/wp-admin/includes/export.php + - + identifier: function.inner + path: src/wp-admin/includes/export.php + count: 13 + - + identifier: function.inner + path: src/wp-admin/includes/file.php + count: 1 + - + identifier: function.inner + path: src/wp-includes/canonical.php + count: 1 diff --git a/src/wp-admin/includes/class-wp-filesystem-ssh2.php b/src/wp-admin/includes/class-wp-filesystem-ssh2.php index 9e0cb885b0bcc..30bd38c3cf2f2 100644 --- a/src/wp-admin/includes/class-wp-filesystem-ssh2.php +++ b/src/wp-admin/includes/class-wp-filesystem-ssh2.php @@ -672,6 +672,7 @@ public function size( $file ) { * Default 0. */ public function touch( $file, $time = 0, $atime = 0 ) { + // @phpstan-ignore-next-line // Not implemented. } diff --git a/src/wp-admin/press-this.php b/src/wp-admin/press-this.php index c91df1c96b84b..45021964364a3 100644 --- a/src/wp-admin/press-this.php +++ b/src/wp-admin/press-this.php @@ -22,8 +22,8 @@ function wp_load_press_this() { 403 ); } elseif ( is_plugin_active( $plugin_file ) ) { - include WP_PLUGIN_DIR . '/press-this/class-wp-press-this-plugin.php'; - $wp_press_this = new WP_Press_This_Plugin(); + include WP_PLUGIN_DIR . '/press-this/class-wp-press-this-plugin.php'; // @phpstan-ignore include.fileNotFound + $wp_press_this = new WP_Press_This_Plugin(); // @phpstan-ignore class.notFound $wp_press_this->html(); } elseif ( current_user_can( 'activate_plugins' ) ) { if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin_file ) ) { diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 598f3ba918536..1e91fd3c39876 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -3365,7 +3365,7 @@ public function get_svg_filters( $origins ) { * @param array $theme_json The theme.json like structure to inspect. * @param array $path Path to inspect. * @param bool|array $override Data to compute whether to override the preset. - * @return bool + * @return bool|null True if the preset should override the defaults, false if not. Null if the override parameter is invalid. */ protected static function should_override_preset( $theme_json, $path, $override ) { _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); @@ -3400,6 +3400,8 @@ protected static function should_override_preset( $theme_json, $path, $override return true; } + + return null; } /** diff --git a/src/wp-includes/customize/class-wp-customize-background-image-setting.php b/src/wp-includes/customize/class-wp-customize-background-image-setting.php index f56810e6aab4b..641540660c45d 100644 --- a/src/wp-includes/customize/class-wp-customize-background-image-setting.php +++ b/src/wp-includes/customize/class-wp-customize-background-image-setting.php @@ -28,6 +28,7 @@ final class WP_Customize_Background_Image_Setting extends WP_Customize_Setting { * @since 3.4.0 * * @param mixed $value The value to update. Not used. + * @return bool|void Nothing is returned. */ public function update( $value ) { remove_theme_mod( 'background_image_thumb' ); diff --git a/src/wp-includes/customize/class-wp-customize-filter-setting.php b/src/wp-includes/customize/class-wp-customize-filter-setting.php index ad70f4f853288..cf0ce2b2fb7ab 100644 --- a/src/wp-includes/customize/class-wp-customize-filter-setting.php +++ b/src/wp-includes/customize/class-wp-customize-filter-setting.php @@ -24,6 +24,7 @@ class WP_Customize_Filter_Setting extends WP_Customize_Setting { * @since 3.4.0 * * @param mixed $value The value to update. + * @return bool|void Nothing is returned. */ public function update( $value ) {} } diff --git a/src/wp-includes/customize/class-wp-customize-header-image-setting.php b/src/wp-includes/customize/class-wp-customize-header-image-setting.php index 80333a54128af..cdf5128322717 100644 --- a/src/wp-includes/customize/class-wp-customize-header-image-setting.php +++ b/src/wp-includes/customize/class-wp-customize-header-image-setting.php @@ -32,6 +32,7 @@ final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting { * @global Custom_Image_Header $custom_image_header * * @param mixed $value The value to update. + * @return bool|void Nothing is returned. */ public function update( $value ) { global $custom_image_header; diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index be41add6590b6..36fe095cb8394 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -4118,7 +4118,7 @@ function get_taxonomies_for_attachments( $output = 'names' ) { * false otherwise. */ function is_gd_image( $image ) { - if ( $image instanceof GdImage + if ( $image instanceof GdImage // @phpstan-ignore class.notFound (Only available with PHP8+.) || is_resource( $image ) && 'gd' === get_resource_type( $image ) ) { return true; diff --git a/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php b/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php index 4a82f28b8a41e..4cc7546cf39e5 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php +++ b/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php @@ -56,6 +56,7 @@ public static function get_store( $store_name = 'default' ) { return; } if ( ! isset( static::$stores[ $store_name ] ) ) { + // @phpstan-ignore new.static (In PHPStan 2.x we can enforce with `@phpstan-consistent-constructor`) static::$stores[ $store_name ] = new static(); // Set the store name. static::$stores[ $store_name ]->set_name( $store_name ); diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 81b35fadf4883..3eda413710494 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -796,7 +796,7 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { } if ( isset( $s ) ) { - $s = esc_attr( $s ); + $s = esc_attr( $s ); // @phpstan-ignore variable.undefined (It's extracted from query vars.) } /** diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md new file mode 100644 index 0000000000000..8776ba37e6f8d --- /dev/null +++ b/tests/phpstan/README.md @@ -0,0 +1,84 @@ +# PHPStan + +PHPStan is a static analysis tool for PHP that checks your code for errors without needing to execute the specific lines or write extra tests. + +## Running the tests + +PHPStan requires PHP and Composer dependencies to be installed. + +If you don't already have an environment ready, you can set one up by following [these instructions](https://github.com/WordPress/wordpress-develop/blob/master/README.md). + +Then you can launch the tests by running: + +``` +npm run test:php:stan +``` + +which will run PHPStan in the Docker container. + +Additional flags supported by PHPStan can be passed by passing `--` followed by the flags themselves. For example, + +``` +# to increase the memory limit from the default 2G to 4G: +npm run test:php:stan -- --memory-limit=4G + +# to analyse only a specific file: +npm run test:php:stan -- tests/phpstan/src/wp-includes/template.php + +# To scan with verbose debugging output: +npm run test:php:stan -- -vvv --debug + +``` + +If you are not using the Docker environment, you can run PHPStan via Composer directly: + +``` +composer run analyse + +compose run analyse -- --memory-limit=4G +compose run analyse -- tests/phpstan/src/wp-includes/template.php +compose run analyse -- -vvv --debug +``` + +For available flags, see https://phpstan.org/user-guide/command-line-usage. + +## The PHPStan configuration + +The PHPStan configuration file is located at [`phpstan.neon.dist`](../../phpstan.neon.dist). + +You can create a local copy at `phpstan.neon` to override the default configuration. + +For more information about configuring PHPStan, see the [PHPStan documentation's Config reference](https://phpstan.org/config-reference). + +## Ignoring errors + +As we adopt PHPStan iteratively, you may be faced with false positives due to legacy code, or code that is not worth changing at this time. + +PHPStan errors can be ignored in the following ways: + +- Using the `@phpstan-ignore {error-identifier} (Reason for ignoring)` annotation in the code. This should be used when addressing false positives. + +- Adding the error pattern to the `ignoreErrors` section of the `phpstan.neon` configuration file. This should be used when addressing conflicts between WordPress coding standards, or legacy code that is not worth refactoring just to satisfy the tests. + +- Adding an error to the "tech debt" baseline. This should be used for code that needs to be addressed eventually - by fixing, refactoring, or ignoring via one of the above methods - but is not worth addressing right now. + + Baselines are a useful triage tool for handling PHPStan errors in legacy code, as they allow us to enforce stricter code quality checks on new code, while gradually chipping away at the existing issues over time. You should avoid adding PHPStan errors from new code whenever possible. + + Baselining is done by running: + + ``` + npm run test:php:stan -- --generate-baseline=tests/phpstan/baseline.php + + # or, with Composer directly: + composer run analyse -- --generate-baseline=tests/phpstan/baseline.php + ``` + + This will regenerate the baseline file with any new errors added to the existing ones. You can then commit the updated baseline file. + +## Performance and troubleshooting + +PHPStan can be resource-intensive, especially on large codebases like WordPress. If you encounter memory limit issues, you can increase the memory limit by passing the `--memory-limit` flag as shown above. + +PHPStan caches analysis results to speed up subsequent runs. You can see information about the results cache by running `analyse` with the `-vv` flag. + +Sometimes, due to the lack of type information in legacy code, PHPStan may still struggle to analyse certain parts of the codebase. In such cases, you can use the `--debug` flag to disable caching and see which files are causing issues. diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon new file mode 100644 index 0000000000000..92372d0e4ded9 --- /dev/null +++ b/tests/phpstan/base.neon @@ -0,0 +1,125 @@ +# Base PHPStan configuration for WordPress Core. +# +# This is kept separate from the main PHPStan configuration file to allow for easy overloading while baseline errors are being fixed. +# +# https://phpstan.org/config-reference + +parameters: + # Cache is stored locally, so it's available for CI. + tmpDir: ../../.cache + + # The Minimum PHP Version + phpVersion: 70224 + + # If it's not enforced by PHP we can't assume users are passing valid values. + treatPhpDocTypesAsCertain: false + + # These config options are explained in https://phpstan.org/config-reference + checkFunctionNameCase: true + inferPrivatePropertyTypeFromConstructor: true + + # Constants whose values may differ depending on the install. + dynamicConstantNames: + - ALLOW_SUBDIRECTORY_INSTALL + - AUTH_SALT + - AUTOMATIC_UPDATER_DISABLED + - COOKIEPATH + - CUSTOM_TAGS + - DISALLOW_FILE_EDIT + - DISALLOW_UNFILTERED_HTML + - EMPTY_TRASH_DAYS + - ENFORCE_GZIP + - FORCE_SSL_LOGIN + - MEDIA_TRASH + - MULTISITE + - NOBLOGREDIRECT + - SAVEQUERIES + - SCRIPT_DEBUG + - SECRET_KEY + - SECRET_SALT + - SHORTINIT + - SITECOOKIEPATH + - UPLOADBLOGSDIR + - WP_ALLOW_MULTISITE + - WP_CACHE + - WP_DEBUG + - WP_DEBUG_DISPLAY + - WP_DEBUG_LOG + - WP_LANG_DIR + - WP_NETWORK_ADMIN + - WP_POST_REVISIONS + - WP_SITEURL + - WP_USE_THEMES + - WP_USER_ADMIN + - WPLANG + - WPMU_ACCEL_REDIRECT + - WPMU_PLUGIN_DIR + - WPMU_SENDFILE + + # What directories and files should be scanned. + paths: + - ../../src + bootstrapFiles: + - bootstrap.php + scanFiles: + - ../../wp-config-sample.php + - ../../src/wp-admin/includes/ms.php + scanDirectories: + - ../../src/wp-includes + - ../../src/wp-admin + excludePaths: + analyseAndScan: + - ../../src/wp-admin/includes/noop.php + # These files are not part of the WordPress Core codebase. + - ../../src/wp-content + # JavaScript/CSS/Asset files. + - ../../src/wp-admin/css + - ../../src/wp-admin/images + # These are built from js/_enqueues. + - ../../src/wp-admin/js (?) + - ../../src/wp-includes/js (?) + analyse: + # These files are deprecated. + - ../../src/wp-admin/includes/deprecated.php + - ../../src/wp-admin/includes/ms-deprecated.php + - ../../src/wp-includes/deprecated.php + - ../../src/wp-includes/ms-deprecated.php + - ../../src/wp-includes/pluggable-deprecated.php + # These files are sourced by wordpress/gutenberg in `tools/release/sync-stable-blocks.js`. + - ../../src/wp-includes/blocks + # Third-party libraries. + - ../../src/js/_enqueues/vendor + - ../../src/wp-admin/includes/class-ftp-pure.php + - ../../src/wp-admin/includes/class-ftp-sockets.php + - ../../src/wp-admin/includes/class-ftp.php + - ../../src/wp-admin/includes/class-pclzip.php + - ../../src/wp-includes/atomlib.php + - ../../src/wp-includes/class-avif-info.php + - ../../src/wp-includes/class-IXR.php + - ../../src/wp-includes/class-json.php + - ../../src/wp-includes/class-phpass.php + - ../../src/wp-includes/class-pop3.php + - ../../src/wp-includes/class-requests.php + - ../../src/wp-includes/class-simplepie.php + - ../../src/wp-includes/class-snoopy.php + - ../../src/wp-includes/class-wp-feed-cache.php + - ../../src/wp-includes/class-wp-http-ixr-client.php + - ../../src/wp-includes/class-wp-http-requests-hooks.php + - ../../src/wp-includes/class-wp-http-requests-response.php + - ../../src/wp-includes/class-wp-simplepie-file.php + - ../../src/wp-includes/class-wp-simplepie-sanitize-kses.php + - ../../src/wp-includes/class-wp-text-diff-renderer-inline.php + - ../../src/wp-includes/class-wp-text-diff-renderer-table.php + - ../../src/wp-includes/rss.php + - ../../src/wp-includes/ID3 + - ../../src/wp-includes/IXR + - ../../src/wp-includes/PHPMailer + - ../../src/wp-includes/pomo + - ../../src/wp-includes/Requests + - ../../src/wp-includes/SimplePie + - ../../src/wp-includes/sodium_compat + - ../../src/wp-includes/Text + # Contains errors that cannot be ignored by PHPStan. + - ../../src/wp-includes/html-api/class-wp-html-processor.php + # Setting `$metadata['user_pass'] = ''` (https://core.trac.wordpress.org/ticket/22114) causes PHPStan to hang + - ../../src/wp-includes/user.php diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php new file mode 100644 index 0000000000000..646cbdbef630c --- /dev/null +++ b/tests/phpstan/baseline.php @@ -0,0 +1,3 @@ + Date: Fri, 16 Jan 2026 18:55:49 +0200 Subject: [PATCH 02/31] chore: phpstan v2 and post merge cleanup --- .github/workflows/php-static-analysis.yml | 4 ---- composer.json | 2 +- phpstan.neon.dist | 5 +++-- src/wp-includes/class-wp-scripts.php | 2 +- .../style-engine/class-wp-style-engine-css-rules-store.php | 3 ++- tests/phpstan/base.neon | 4 +++- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/php-static-analysis.yml b/.github/workflows/php-static-analysis.yml index 9b15ee6f9f0fe..cc151c58f484b 100644 --- a/.github/workflows/php-static-analysis.yml +++ b/.github/workflows/php-static-analysis.yml @@ -5,17 +5,13 @@ on: push: branches: - trunk - - '6.9' - '[7-9].[0-9]' tags: - - '6.9' - - '6.9.[0-9]+' - '[7-9].[0-9]' - '[7-9]+.[0-9].[0-9]+' pull_request: branches: - trunk - - '6.9' - '[7-9].[0-9]' paths: # This workflow only scans PHP files. diff --git a/composer.json b/composer.json index 62ea2fdd8dbba..cd673bc17aaed 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "squizlabs/php_codesniffer": "3.13.5", "wp-coding-standards/wpcs": "~3.3.0", "phpcompatibility/phpcompatibility-wp": "~2.1.3", - "phpstan/phpstan": "~1.12.32", + "phpstan/phpstan": "~2.1.33", "yoast/phpunit-polyfills": "^1.1.0" }, "config": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2bffeb442454f..b572bd02f1eb5 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,8 +5,9 @@ # https://phpstan.org/config-reference includes: - # The WordPress Core configuration file includes the base configuration for the WordPress codebase. + # The base configuration file for using PHPStan with the WordPress core codebase. - tests/phpstan/base.neon + # The baseline file includes preexisting errors in the codebase that should be ignored. # https://phpstan.org/user-guide/baseline - tests/phpstan/baseline.php @@ -14,7 +15,7 @@ includes: parameters: # https://phpstan.org/user-guide/rule-levels level: 0 - reportUnmatchedIgnoredErrors: false + reportUnmatchedIgnoredErrors: true ignoreErrors: # Level 0: diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 8c02c5af98e38..ca9fe2dbcedc7 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -1144,7 +1144,7 @@ private function get_highest_fetchpriority_with_dependents( string $handle, arra } } } - $stored_results[ $handle ] = $priorities[ $highest_priority_index ]; // @phpstan-ignore parameterByRef.type (We know the index is valid and that this will be a string.) + $stored_results[ $handle ] = $priorities[ $highest_priority_index ]; return $priorities[ $highest_priority_index ]; } diff --git a/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php b/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php index 4cc7546cf39e5..220980c8dc6b6 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php +++ b/src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php @@ -13,6 +13,8 @@ * Holds, sanitizes, processes, and prints CSS declarations for the style engine. * * @since 6.1.0 + * + * @phpstan-consistent-constructor */ #[AllowDynamicProperties] class WP_Style_Engine_CSS_Rules_Store { @@ -56,7 +58,6 @@ public static function get_store( $store_name = 'default' ) { return; } if ( ! isset( static::$stores[ $store_name ] ) ) { - // @phpstan-ignore new.static (In PHPStan 2.x we can enforce with `@phpstan-consistent-constructor`) static::$stores[ $store_name ] = new static(); // Set the store name. static::$stores[ $store_name ]->set_name( $store_name ); diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 92372d0e4ded9..3036ed3375930 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -9,7 +9,9 @@ parameters: tmpDir: ../../.cache # The Minimum PHP Version - phpVersion: 70224 + phpVersion: + min: 70400 + max: 80500 # If it's not enforced by PHP we can't assume users are passing valid values. treatPhpDocTypesAsCertain: false From 88c5426ecd9d007c4b4c7d913833c9d1eda05e95 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 16 Jan 2026 19:16:49 +0200 Subject: [PATCH 03/31] chore: cleanup readme --- tests/phpstan/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index 8776ba37e6f8d..3961437a531f9 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -10,7 +10,7 @@ If you don't already have an environment ready, you can set one up by following Then you can launch the tests by running: -``` +```bash npm run test:php:stan ``` @@ -18,7 +18,7 @@ which will run PHPStan in the Docker container. Additional flags supported by PHPStan can be passed by passing `--` followed by the flags themselves. For example, -``` +```bash # to increase the memory limit from the default 2G to 4G: npm run test:php:stan -- --memory-limit=4G @@ -32,7 +32,7 @@ npm run test:php:stan -- -vvv --debug If you are not using the Docker environment, you can run PHPStan via Composer directly: -``` +```bash composer run analyse compose run analyse -- --memory-limit=4G @@ -50,23 +50,23 @@ You can create a local copy at `phpstan.neon` to override the default configurat For more information about configuring PHPStan, see the [PHPStan documentation's Config reference](https://phpstan.org/config-reference). -## Ignoring errors +## Ignoring and baselining errors As we adopt PHPStan iteratively, you may be faced with false positives due to legacy code, or code that is not worth changing at this time. PHPStan errors can be ignored in the following ways: -- Using the `@phpstan-ignore {error-identifier} (Reason for ignoring)` annotation in the code. This should be used when addressing false positives. +- Using the `@phpstan-ignore {error-identifier} (Reason for ignoring)` annotation in the code itself. This should be used to suppress false positives with a specific line of code. -- Adding the error pattern to the `ignoreErrors` section of the `phpstan.neon` configuration file. This should be used when addressing conflicts between WordPress coding standards, or legacy code that is not worth refactoring just to satisfy the tests. +- Adding the error pattern to the `ignoreErrors` section of the `phpstan.neon.dist` configuration file. This should be used handle conflicts with WordPress Coding Standards or similar project decisions, or to allowlist legacy code that is not worth refactoring solely to satisfy the tests. - Adding an error to the "tech debt" baseline. This should be used for code that needs to be addressed eventually - by fixing, refactoring, or ignoring via one of the above methods - but is not worth addressing right now. - Baselines are a useful triage tool for handling PHPStan errors in legacy code, as they allow us to enforce stricter code quality checks on new code, while gradually chipping away at the existing issues over time. You should avoid adding PHPStan errors from new code whenever possible. + Baselines are a useful triage tool for handling PHPStan errors in legacy code, as they allow us to enforce stricter code quality checks on new code, while gradually chipping away at the existing issues over time. **Avoid adding PHPStan errors from new code whenever possible, and use baselines as a last resort.** - Baselining is done by running: + The baseline file is located at `tests/phpstan/baseline.php` and generated by running PHPStan with the `--generate-baseline` flag: - ``` + ```bash npm run test:php:stan -- --generate-baseline=tests/phpstan/baseline.php # or, with Composer directly: @@ -77,8 +77,8 @@ PHPStan errors can be ignored in the following ways: ## Performance and troubleshooting -PHPStan can be resource-intensive, especially on large codebases like WordPress. If you encounter memory limit issues, you can increase the memory limit by passing the `--memory-limit` flag as shown above. +PHPStan can be resource-intensive, especially on large codebases like WordPress. If you encounter memory limit issues, you can increase the memory limit by passing the `--memory-limit` flag as shown [above](#running-the-tests). -PHPStan caches analysis results to speed up subsequent runs. You can see information about the results cache by running `analyse` with the `-vv` flag. +PHPStan caches analysis results to speed up subsequent runs. You can see information about the results cache by running `analyse` with the `-vv` or `-vvv` flag. Sometimes, due to the lack of type information in legacy code, PHPStan may still struggle to analyse certain parts of the codebase. In such cases, you can use the `--debug` flag to disable caching and see which files are causing issues. From 03b4080e0e1509747ff7fae67c0c1566a51e9d0d Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 16 Jan 2026 19:43:08 +0200 Subject: [PATCH 04/31] tests: remove unnecessary @phpstan-ignore --- src/wp-includes/template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 3eda413710494..6ec1934f866ec 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -976,7 +976,7 @@ static function ( int $level, string $message, ?string $file = null, ?int $line } // Display a caught exception as an error since it prevents any of the output buffer filters from applying. - if ( $did_just_catch ) { // @phpstan-ignore if.alwaysFalse (The variable is set in the catch block below.) + if ( $did_just_catch ) { $level = E_USER_ERROR; } From cd1149ab2891f7b0f02db6ae51b3fe021f92cdfb Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 16 Jan 2026 20:06:53 +0200 Subject: [PATCH 05/31] docs: add `never|void` return type to `wp_die()` --- phpstan.neon.dist | 2 +- src/wp-includes/functions.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b572bd02f1eb5..e74e6ec1a441b 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -19,7 +19,7 @@ parameters: ignoreErrors: # Level 0: - - # Inner functions arent supported by PHPstan. + - # Inner functions aren't supported by PHPStan. message: '#Function wxr_[a-z_]+ not found#' path: src/wp-admin/includes/export.php - diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 9cdeef75788f2..c55c08b96aa78 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -3765,6 +3765,10 @@ function wp_nonce_ays( $action ) { * is a WP_Error. * @type bool $exit Whether to exit the process after completion. Default true. * } + * + * @return never|void Returns false if `$args['exit']` is false, otherwise exists. + * + * @phpstan-return ($args['exit'] is false ? void : never) */ function wp_die( $message = '', $title = '', $args = array() ) { global $wp_query; From aec7e74531b5470c390ac982596976de9b081ae4 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Sat, 17 Jan 2026 00:19:51 +0200 Subject: [PATCH 06/31] ci: run `build:dev` https://core.trac.wordpress.org/ticket/64393 --- .../reusable-php-static-analysis.yml | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-php-static-analysis.yml b/.github/workflows/reusable-php-static-analysis.yml index 5e8d592ed2c62..aa6e6db1c9729 100644 --- a/.github/workflows/reusable-php-static-analysis.yml +++ b/.github/workflows/reusable-php-static-analysis.yml @@ -45,6 +45,12 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false + - name: Set up Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version-file: '.nvmrc' + cache: npm + - name: Set up PHP uses: shivammathur/setup-php@20529878ed81ef8e78ddf08b480401e6101a850f # v2.35.3 with: @@ -52,16 +58,18 @@ jobs: coverage: none tools: cs2pr - - name: Log debug information - run: | - composer --version - # This date is used to ensure that the Composer cache is cleared at least once every week. # http://man7.org/linux/man-pages/man1/date.1.html - name: "Get last Monday's date" id: get-date run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" + - name: General debug information + run: | + npm --version + node --version + composer --version + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, # passing a custom cache suffix ensures that the cache is flushed at least once per week. - name: Install Composer dependencies @@ -72,6 +80,12 @@ jobs: - name: Make Composer packages available globally run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" + - name: Install npm dependencies + run: npm ci + + - name: Build WordPress + run: npm run build:dev + - name: Cache PHP Static Analysis scan cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: From d4291813be8ae6befa6aec3d62321b0f1aec1c7a Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Thu, 12 Feb 2026 23:53:30 +0200 Subject: [PATCH 07/31] Update src/wp-includes/functions.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index c55c08b96aa78..282d8934030a4 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -3766,7 +3766,7 @@ function wp_nonce_ays( $action ) { * @type bool $exit Whether to exit the process after completion. Default true. * } * - * @return never|void Returns false if `$args['exit']` is false, otherwise exists. + * @return never|void Returns false if `$args['exit']` is false, otherwise exits. * * @phpstan-return ($args['exit'] is false ? void : never) */ From de6d3049dbf07e766567264ffd9354048b3d59ed Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Thu, 12 Feb 2026 23:54:15 +0200 Subject: [PATCH 08/31] Update tests/phpstan/bootstrap.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpstan/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpstan/bootstrap.php b/tests/phpstan/bootstrap.php index 7be626b3dc1f3..c87a26babf83d 100644 --- a/tests/phpstan/bootstrap.php +++ b/tests/phpstan/bootstrap.php @@ -2,7 +2,7 @@ /** * Defines default WordPress constants for discovery. * - * Mocks the constant initiation that would normally happen in wp-includes/wp-setttings.php. + * Mocks the constant initiation that would normally happen in wp-includes/wp-settings.php. */ // wp_initial_constants() From d43edb1e7ef3df371351700ea5cf3bf00cd07c39 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Thu, 12 Feb 2026 23:54:34 +0200 Subject: [PATCH 09/31] Update tests/phpstan/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpstan/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index 3961437a531f9..dacde33712487 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -35,9 +35,9 @@ If you are not using the Docker environment, you can run PHPStan via Composer di ```bash composer run analyse -compose run analyse -- --memory-limit=4G -compose run analyse -- tests/phpstan/src/wp-includes/template.php -compose run analyse -- -vvv --debug +composer run analyse -- --memory-limit=4G +composer run analyse -- tests/phpstan/src/wp-includes/template.php +composer run analyse -- -vvv --debug ``` For available flags, see https://phpstan.org/user-guide/command-line-usage. From 8e5e8b044ee787f050837c58448abb57c95b1a96 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Thu, 12 Feb 2026 23:54:47 +0200 Subject: [PATCH 10/31] Update .github/workflows/php-static-analysis.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/php-static-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php-static-analysis.yml b/.github/workflows/php-static-analysis.yml index cc151c58f484b..056008b7571d0 100644 --- a/.github/workflows/php-static-analysis.yml +++ b/.github/workflows/php-static-analysis.yml @@ -40,7 +40,7 @@ permissions: {} jobs: # Runs PHPStan Static Analysis. phpstan: - name: PHP coding standards + name: PHP static analysis uses: ./.github/workflows/reusable-php-static-analysis.yml permissions: contents: read From 4c255e5395ed24680ee9d7bd7efa27a0cfa3fb78 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 16 Feb 2026 13:44:20 -0800 Subject: [PATCH 11/31] PHPStan: Use explicit paths to avoid hanging on wp-content traversal By listing specific directories and files instead of the entire 'src' directory, we bypass the expensive directory traversal of 'wp-content', which was causing PHPStan to hang even when excluded. This update includes wp-admin, wp-includes, root PHP files, and the default themes. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/phpstan/base.neon | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 3036ed3375930..1994819498565 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -60,7 +60,36 @@ parameters: # What directories and files should be scanned. paths: - - ../../src + - ../../src/wp-admin + - ../../src/wp-includes + - ../../src/wp-content/themes/twentyeleven + - ../../src/wp-content/themes/twentyfifteen + - ../../src/wp-content/themes/twentyfourteen + - ../../src/wp-content/themes/twentynineteen + - ../../src/wp-content/themes/twentyseventeen + - ../../src/wp-content/themes/twentysixteen + - ../../src/wp-content/themes/twentyten + - ../../src/wp-content/themes/twentythirteen + - ../../src/wp-content/themes/twentytwelve + - ../../src/wp-content/themes/twentytwenty + - ../../src/wp-content/themes/twentytwentyfive + - ../../src/wp-content/themes/twentytwentyfour + - ../../src/wp-content/themes/twentytwentyone + - ../../src/wp-content/themes/twentytwentythree + - ../../src/wp-content/themes/twentytwentytwo + - ../../src/index.php + - ../../src/wp-activate.php + - ../../src/wp-blog-header.php + - ../../src/wp-comments-post.php + - ../../src/wp-cron.php + - ../../src/wp-links-opml.php + - ../../src/wp-load.php + - ../../src/wp-login.php + - ../../src/wp-mail.php + - ../../src/wp-settings.php + - ../../src/wp-signup.php + - ../../src/wp-trackback.php + - ../../src/xmlrpc.php bootstrapFiles: - bootstrap.php scanFiles: @@ -72,8 +101,6 @@ parameters: excludePaths: analyseAndScan: - ../../src/wp-admin/includes/noop.php - # These files are not part of the WordPress Core codebase. - - ../../src/wp-content # JavaScript/CSS/Asset files. - ../../src/wp-admin/css - ../../src/wp-admin/images From 522147a4a594f6f82febc67c255ce8446863dd49 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 16 Feb 2026 13:49:48 -0800 Subject: [PATCH 12/31] Remove excludePaths for non-PHP directories since files already excluded --- tests/phpstan/base.neon | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 1994819498565..be029c9abafce 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -101,12 +101,6 @@ parameters: excludePaths: analyseAndScan: - ../../src/wp-admin/includes/noop.php - # JavaScript/CSS/Asset files. - - ../../src/wp-admin/css - - ../../src/wp-admin/images - # These are built from js/_enqueues. - - ../../src/wp-admin/js (?) - - ../../src/wp-includes/js (?) analyse: # These files are deprecated. - ../../src/wp-admin/includes/deprecated.php @@ -117,7 +111,6 @@ parameters: # These files are sourced by wordpress/gutenberg in `tools/release/sync-stable-blocks.js`. - ../../src/wp-includes/blocks # Third-party libraries. - - ../../src/js/_enqueues/vendor - ../../src/wp-admin/includes/class-ftp-pure.php - ../../src/wp-admin/includes/class-ftp-sockets.php - ../../src/wp-admin/includes/class-ftp.php From baf451609a0ccea44476bac173bb34f8414e570f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 16 Feb 2026 14:02:11 -0800 Subject: [PATCH 13/31] Address issues with class-wp-html-processor.php which required it to be ignored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed: 360 Unsafe usage of new static(). 🪪 new.static 💡 See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static at src/wp-includes/html-api/class-wp-html-processor.php:360 523 Unsafe usage of new static(). 🪪 new.static 💡 See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static at src/wp-includes/html-api/class-wp-html-processor.php:523 1072 Method WP_HTML_Processor::step() should return bool but return statement is missing. at src/wp-includes/html-api/class-wp-html-processor.php:1072 3452 Method WP_HTML_Processor::step_in_table() should return bool but return statement is missing. at src/wp-includes/html-api/class-wp-html-processor.php:3452 3471 Method WP_HTML_Processor::step_in_table_text() should return bool but return statement is missing. at src/wp-includes/html-api/class-wp-html-processor.php:3471 5903 Method WP_HTML_Processor::reconstruct_active_formatting_elements() should return bool but return statement is missing. at src/wp-includes/html-api/class-wp-html-processor.php:5903 --- src/wp-includes/html-api/class-wp-html-processor.php | 2 ++ tests/phpstan/base.neon | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 55f955f2c1a9a..2dfba365343da 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -139,6 +139,7 @@ * * @see WP_HTML_Tag_Processor * @see https://html.spec.whatwg.org/ + * @phpstan-consistent-constructor */ class WP_HTML_Processor extends WP_HTML_Tag_Processor { /** @@ -583,6 +584,7 @@ private function create_fragment_at_current_node( string $html ) { * @since 6.7.0 * * @param string $message Explains support is missing in order to parse the current node. + * @return never */ private function bail( string $message ) { $here = $this->bookmarks[ $this->state->current_token->bookmark_name ]; diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index be029c9abafce..a2b2a0578b592 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -141,7 +141,5 @@ parameters: - ../../src/wp-includes/SimplePie - ../../src/wp-includes/sodium_compat - ../../src/wp-includes/Text - # Contains errors that cannot be ignored by PHPStan. - - ../../src/wp-includes/html-api/class-wp-html-processor.php # Setting `$metadata['user_pass'] = ''` (https://core.trac.wordpress.org/ticket/22114) causes PHPStan to hang - ../../src/wp-includes/user.php From 16038236c2e26f907f367a748f871723ec0925c2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 16 Feb 2026 14:09:06 -0800 Subject: [PATCH 14/31] Fix return types for core themes --- .../themes/twentyfourteen/inc/featured-content.php | 2 +- src/wp-content/themes/twentytwenty/inc/template-tags.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-content/themes/twentyfourteen/inc/featured-content.php b/src/wp-content/themes/twentyfourteen/inc/featured-content.php index b98ab983919df..3193aa8b93549 100644 --- a/src/wp-content/themes/twentyfourteen/inc/featured-content.php +++ b/src/wp-content/themes/twentyfourteen/inc/featured-content.php @@ -212,7 +212,7 @@ public static function delete_transient() { * @since Twenty Fourteen 1.0 * * @param WP_Query $query WP_Query object. - * @return WP_Query Possibly-modified WP_Query. + * @return void */ public static function pre_get_posts( $query ) { diff --git a/src/wp-content/themes/twentytwenty/inc/template-tags.php b/src/wp-content/themes/twentytwenty/inc/template-tags.php index fdf51ccee9624..e15ae6652bbec 100644 --- a/src/wp-content/themes/twentytwenty/inc/template-tags.php +++ b/src/wp-content/themes/twentytwenty/inc/template-tags.php @@ -29,7 +29,7 @@ * * @param array $args Arguments for displaying the site logo either as an image or text. * @param bool $display Display or return the HTML. - * @return string Compiled HTML based on our arguments. + * @return string|void Compiled HTML based on our arguments. */ function twentytwenty_site_logo( $args = array(), $display = true ) { $logo = get_custom_logo(); @@ -107,7 +107,7 @@ function twentytwenty_site_logo( $args = array(), $display = true ) { * @since Twenty Twenty 1.0 * * @param bool $display Display or return the HTML. - * @return string The HTML to display. + * @return string|void The HTML to display. */ function twentytwenty_site_description( $display = true ) { $description = get_bloginfo( 'description' ); @@ -249,7 +249,7 @@ function twentytwenty_edit_post_link( $link, $post_id, $text ) { * * @param int $post_id The ID of the post. * @param string $location The location where the meta is shown. - * @return string Post meta HTML. + * @return string|void Post meta HTML. */ function twentytwenty_get_post_meta( $post_id = null, $location = 'single-top' ) { From 367af1c0f309a04490e21aac32b0ab6be62d9f7f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 16 Feb 2026 14:32:34 -0800 Subject: [PATCH 15/31] Add variable return type for WP_Theme::get() --- .../classes/class-twenty-twenty-one-customize.php | 4 ++-- .../classes/class-twenty-twenty-one-dark-mode.php | 2 +- src/wp-includes/class-wp-theme.php | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-customize.php b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-customize.php index 3ed6b637af698..d482aa940cf8f 100644 --- a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-customize.php +++ b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-customize.php @@ -35,8 +35,8 @@ public function __construct() { public function register( $wp_customize ) { // Change site-title & description to postMessage. - $wp_customize->get_setting( 'blogname' )->transport = 'postMessage'; // @phpstan-ignore-line. Assume that this setting exists. - $wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage'; // @phpstan-ignore-line. Assume that this setting exists. + $wp_customize->get_setting( 'blogname' )->transport = 'postMessage'; // @phpstan-ignore property.nonObject (Assume that this setting exists.) + $wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage'; // @phpstan-ignore property.nonObject (Assume that this setting exists.) // Add partial for blogname. $wp_customize->selective_refresh->add_partial( diff --git a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php index c5643ade65da4..abff7d3de9fe1 100644 --- a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php +++ b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php @@ -98,7 +98,7 @@ public function enqueue_scripts() { if ( is_rtl() ) { $url = get_template_directory_uri() . '/assets/css/style-dark-mode-rtl.css'; } - wp_enqueue_style( 'tt1-dark-mode', $url, array( 'twenty-twenty-one-style' ), wp_get_theme()->get( 'Version' ) ); // @phpstan-ignore-line. Version is always a string. + wp_enqueue_style( 'tt1-dark-mode', $url, array( 'twenty-twenty-one-style' ), wp_get_theme()->get( 'Version' ) ); } /** diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php index c894f6dd72f40..0f3271f28a7dd 100644 --- a/src/wp-includes/class-wp-theme.php +++ b/src/wp-includes/class-wp-theme.php @@ -864,6 +864,8 @@ public function cache_delete() { * * @since 3.4.0 * + * @phpstan-return ( $header is 'Tags' ? string[]|false : ( $header is 'Name'|'ThemeURI'|'Description'|'Author'|'AuthorURI'|'Version'|'Template'|'Status'|'TextDomain'|'DomainPath'|'RequiresWP'|'RequiresPHP'|'UpdateURI' ? string|false : mixed ) ) + * * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. * @return string|array|false String or array (for Tags header) on success, false on failure. */ From ae6c4b6f5f1d0822e5df9c74b78dcff8538102cd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 16 Feb 2026 14:33:25 -0800 Subject: [PATCH 16/31] Ensure Customizer setting exists before setting transport to postMessage --- .../classes/class-twenty-twenty-one-customize.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-customize.php b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-customize.php index d482aa940cf8f..bf64d609b87c0 100644 --- a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-customize.php +++ b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-customize.php @@ -35,8 +35,12 @@ public function __construct() { public function register( $wp_customize ) { // Change site-title & description to postMessage. - $wp_customize->get_setting( 'blogname' )->transport = 'postMessage'; // @phpstan-ignore property.nonObject (Assume that this setting exists.) - $wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage'; // @phpstan-ignore property.nonObject (Assume that this setting exists.) + foreach ( array( 'blogname', 'blogdescription' ) as $setting_id ) { + $setting = $wp_customize->get_setting( $setting_id ); + if ( $setting ) { + $setting->transport = 'postMessage'; + } + } // Add partial for blogname. $wp_customize->selective_refresh->add_partial( From 9d178c6d8910e5aaa5f602c35ea97be9d75f4180 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 16 Feb 2026 14:34:21 -0800 Subject: [PATCH 17/31] Pass empty strings instead of null in twenty_twenty_one_generate_css() --- .../themes/twentytwentyone/inc/template-functions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-content/themes/twentytwentyone/inc/template-functions.php b/src/wp-content/themes/twentytwentyone/inc/template-functions.php index 689b1e22c9814..d1d36162b4ba5 100644 --- a/src/wp-content/themes/twentytwentyone/inc/template-functions.php +++ b/src/wp-content/themes/twentytwentyone/inc/template-functions.php @@ -339,12 +339,12 @@ function twenty_twenty_one_get_non_latin_css( $type = 'front-end' ) { } // Return the specified styles. - return twenty_twenty_one_generate_css( // @phpstan-ignore-line. + return twenty_twenty_one_generate_css( implode( ',', $elements[ $type ] ), 'font-family', implode( ',', $font_family[ $locale ] ), - null, - null, + '', + '', false ); } From b1005dc1a6f934c4584ee564f4be13cbbf40f3cf Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 16 Feb 2026 14:35:19 -0800 Subject: [PATCH 18/31] Ensure Twenty_Twenty_One_SVG_Icons::get_svg() always returns string via cast --- .../classes/class-twenty-twenty-one-svg-icons.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-svg-icons.php b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-svg-icons.php index 398ef82bbd7c8..e32d050b4e455 100644 --- a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-svg-icons.php +++ b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-svg-icons.php @@ -189,10 +189,9 @@ public static function get_svg( $group, $icon, $size ) { if ( array_key_exists( $icon, $arr ) ) { $repl = sprintf( '