From 0308f9ee6602ffdde124f271818e61393166c8b5 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 7 Nov 2025 20:44:01 +0200 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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: