diff --git a/.github/workflows/php-static-analysis.yml b/.github/workflows/php-static-analysis.yml
new file mode 100644
index 0000000000000..cc151c58f484b
--- /dev/null
+++ b/.github/workflows/php-static-analysis.yml
@@ -0,0 +1,96 @@
+name: PHPStan Static Analysis
+
+on:
+ # PHPStan testing was introduced in @todo.
+ push:
+ branches:
+ - trunk
+ - '[7-9].[0-9]'
+ tags:
+ - '[7-9].[0-9]'
+ - '[7-9]+.[0-9].[0-9]+'
+ pull_request:
+ branches:
+ - trunk
+ - '[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..aa6e6db1c9729
--- /dev/null
+++ b/.github/workflows/reusable-php-static-analysis.yml
@@ -0,0 +1,109 @@
+##
+# 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 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:
+ php-version: ${{ inputs.php-version }}
+ coverage: none
+ tools: cs2pr
+
+ # 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
+ 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: 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:
+ 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 330a92ca02c7b..bcca11abc8562 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ wp-tests-config.php
/gutenberg
/tests/phpunit/build
/wp-cli.local.yml
+/phpstan.neon
/jsdoc
/composer.lock
/vendor
diff --git a/composer.json b/composer.json
index 2c5b20f7879a9..cd673bc17aaed 100644
--- a/composer.json
+++ b/composer.json
@@ -23,6 +23,7 @@
"squizlabs/php_codesniffer": "3.13.5",
"wp-coding-standards/wpcs": "~3.3.0",
"phpcompatibility/phpcompatibility-wp": "~2.1.3",
+ "phpstan/phpstan": "~2.1.33",
"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 a5d54d773efb6..b697de4de941a 100644
--- a/package.json
+++ b/package.json
@@ -119,6 +119,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..e74e6ec1a441b
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,36 @@
+# 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 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
+
+parameters:
+ # https://phpstan.org/user-guide/rule-levels
+ level: 0
+ reportUnmatchedIgnoredErrors: true
+
+ ignoreErrors:
+ # Level 0:
+ - # Inner functions aren't 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-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/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php
index f9965a754989a..2c88b6863dfde 100644
--- a/src/wp-includes/class-wp-theme-json.php
+++ b/src/wp-includes/class-wp-theme-json.php
@@ -3337,7 +3337,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' );
@@ -3372,6 +3372,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 0834b378d3a6e..d876559c78982 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/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;
diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php
index 6933ad69957e2..92c3f9a8f2819 100644
--- a/src/wp-includes/media.php
+++ b/src/wp-includes/media.php
@@ -4116,7 +4116,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..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 {
diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php
index 81b35fadf4883..6ec1934f866ec 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.)
}
/**
@@ -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;
}
diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md
new file mode 100644
index 0000000000000..3961437a531f9
--- /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:
+
+```bash
+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,
+
+```bash
+# 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:
+
+```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
+```
+
+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 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 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.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. **Avoid adding PHPStan errors from new code whenever possible, and use baselines as a last resort.**
+
+ 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:
+ 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](#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` 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.
diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon
new file mode 100644
index 0000000000000..3036ed3375930
--- /dev/null
+++ b/tests/phpstan/base.neon
@@ -0,0 +1,127 @@
+# 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:
+ min: 70400
+ max: 80500
+
+ # 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 @@
+