From e56c6e8483c667bd56ef2b59369f0b39e50fc092 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Nov 2026 01:43:10 -0700 Subject: [PATCH 1/6] Build/Test Tools: Add phpstan-strict-rules as a dev dependency. This installs phpstan/phpstan-strict-rules 2.1.1 without including its rules.neon in phpstan.neon.dist, so the default analysis and CI are unchanged. Since core does not use phpstan/extension-installer, the rules take effect only when a local phpstan.neon includes vendor/phpstan/phpstan-strict-rules/rules.neon, which lets contributors opt into them, in whole or rule by rule, while the codebase works toward enabling some of them by default. Co-Authored-By: Claude Opus 5.5 (medium effort) Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0 --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 1586264a73f78..70928671aa6e3 100644 --- a/composer.json +++ b/composer.json @@ -54,6 +54,7 @@ "phpcompatibility/phpcompatibility-wp": "~2.1.3", "phpstan/phpstan": "2.3.4", "phpstan/phpstan-phpunit": "2.1.1", + "phpstan/phpstan-strict-rules": "2.1.1", "yoast/phpunit-polyfills": "^1.1.0" }, "config": { From 89ae6958433719b6b45079f4c4ccac79a2e61e51 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Nov 2026 00:38:01 -0700 Subject: [PATCH 2/6] Build/Test Tools: Avoid short ternary in HashNotationVisitor. The WordPress Coding Standards say the short ternary operator must not be used. Replace the `?:` fallback for a failed preg_split() with an explicit false check, so the disallowedShortTernary rule from phpstan-strict-rules can be enabled. Co-Authored-By: Claude Opus 5.5 (medium effort) Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0 --- tests/phpstan/HashNotationVisitor.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/phpstan/HashNotationVisitor.php b/tests/phpstan/HashNotationVisitor.php index 655a494f344f6..e03d32e322a19 100644 --- a/tests/phpstan/HashNotationVisitor.php +++ b/tests/phpstan/HashNotationVisitor.php @@ -484,7 +484,12 @@ private function split_tags( string $text ): array { $tags = array(); $current = null; - foreach ( preg_split( '#\R#', (string) $body ) ?: array() as $line ) { + $lines = preg_split( '#\R#', (string) $body ); + if ( false === $lines ) { + $lines = array(); + } + + foreach ( $lines as $line ) { $line = (string) preg_replace( '#^\s*\*[ ]?#', '', $line, 1 ); if ( preg_match( '#^@([a-zA-Z][a-zA-Z0-9_-]*)[ \t]*(.*)$#', $line, $matches ) === 1 ) { From a18bad400b757fcaf569701b7e3f0d63f013b236 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Nov 2026 00:38:20 -0700 Subject: [PATCH 3/6] Build/Test Tools: Enable strict rules core already complies with. Include phpstan-strict-rules in phpstan.neon.dist with all rules disabled, then enable the ones which report no errors on core today: * disallowedBacktick and disallowedShortTernary, which the WordPress Coding Standards prohibit outright (and PHPCS also enforces). * closureUsesThis, matchingInheritedMethodNames, and numericOperandsInArithmeticOperators, which guard against regressions. booleansInLoopConditions is left disabled: it is normally gated on bleedingEdge, and setting it to true bypasses that gate, which reports 32 errors. It belongs with booleansInConditions, which is far from compliant. Including rules.neon also makes several PHPStan parameters stricter regardless of strictRules.allRules, which would add roughly 300 errors (mostly method signature variance, which cannot be fixed without changing signatures plugins override). Those parameters are reset to the PHPStan defaults. Co-Authored-By: Claude Opus 5.5 (medium effort) Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0 --- phpstan.neon.dist | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 655bb9bbc807c..87b0d96192bd9 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -14,6 +14,10 @@ includes: # new strict rules. - vendor/phpstan/phpstan-phpunit/extension.neon + # Stricter rules, of which only a subset are enabled by default. See the strictRules + # parameter below. + - vendor/phpstan/phpstan-strict-rules/rules.neon + # Preexisting errors that should be ignored, one baseline per error identifier # so that the remaining work on each is visible as a single shrinking file. # Each is meant to reach zero and be deleted, taking its line below with it. @@ -63,6 +67,34 @@ parameters: level: 5 reportUnmatchedIgnoredErrors: true + # Rules from phpstan-strict-rules which core already complies with, or which enforce the + # WordPress Coding Standards. All others are disabled until the code complies with them. + # They can be enabled in a local phpstan.neon to work toward that. + # https://github.com/phpstan/phpstan-strict-rules#enabling-rules-one-by-one + strictRules: + allRules: false + disallowedBacktick: true + disallowedShortTernary: true + closureUsesThis: true + matchingInheritedMethodNames: true + numericOperandsInArithmeticOperators: true + + # Restore the PHPStan defaults of the parameters which phpstan-strict-rules makes stricter, + # since including its rules.neon changes them even when its rules are disabled. + # https://phpstan.org/config-reference#stricter-analysis + polluteScopeWithLoopInitialAssignments: true + polluteScopeWithAlwaysIterableForeach: true + polluteScopeWithBlock: true + checkDynamicProperties: false + checkExplicitMixedMissingReturn: false + checkInternalClassCaseSensitivity: false + reportMaybesInMethodSignatures: false + reportStaticMethodSignatures: false + reportMaybesInPropertyPhpDocTypes: false + reportWrongPhpDocTypeInVarTag: false + checkStrictPrintfPlaceholderTypes: false + reportNonIntStringArrayKey: false + # The following ignored errors are not intended to be fixed, as distinct from the baselines # included above. # From 95fd90a3f3a83e7197457b96bc4e66d7dce4f177 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Nov 2026 10:22:18 -0700 Subject: [PATCH 4/6] Build/Test Tools: Clarify the roles of the PHPStan config files. The base.neon header said it was kept separate to allow for easy overloading while baseline errors are being fixed, meaning a local config could include it alone to skip the baselines. That is no longer needed: a config including phpstan.neon.dist can drop every ignored error with `ignoreErrors!: []`. Describe base.neon instead as the configuration for analyzing the codebase at all, separate from the standards phpstan.neon.dist holds the code to. phpstan.neon.dist and the README also said to copy phpstan.neon.dist to phpstan.neon, which leaves the copy to go stale. Recommend including it instead, and document examples for raising the level, enabling more strict rules, and showing the errors the baselines suppress. Co-Authored-By: Claude Opus 5.5 (medium effort) Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0 --- phpstan.neon.dist | 4 +++- tests/phpstan/README.md | 25 +++++++++++++++++++++++-- tests/phpstan/base.neon | 8 +++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 87b0d96192bd9..6ce7f45fa7294 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,8 @@ # PHPStan configuration for WordPress Core. # -# To overload this configuration, copy this file to phpstan.neon and adjust as needed. +# To override this configuration, create a phpstan.neon which includes this file and adjusts +# its parameters as needed, rather than copying it, so that the copy does not go stale. See +# tests/phpstan/README.md for examples. # # https://phpstan.org/config-reference diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index 6734e780223ca..67fe26178f9b7 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -54,9 +54,30 @@ Some errors cannot be ignored, and so cannot be reported as warnings either. The ## The PHPStan configuration -The PHPStan configuration file is located at [`phpstan.neon.dist`](../../phpstan.neon.dist). +The PHPStan configuration file is located at [`phpstan.neon.dist`](../../phpstan.neon.dist). It sets the standards the code is held to: the rule level, the baselines, the errors ignored as not actionable, and the [strict rules](https://github.com/phpstan/phpstan-strict-rules) enabled. It includes [`base.neon`](base.neon), which configures what PHPStan needs to know to analyze the codebase at all, such as the [WordPress-specific extensions](#wordpress-specific-extensions), the paths analyzed, and the supported PHP versions. -You can create a local copy at `phpstan.neon` to override the default configuration. +To override the default configuration, create a `phpstan.neon` file in the repository root, which PHPStan reads in place of `phpstan.neon.dist`. Include `phpstan.neon.dist` in it rather than copying it, so the override only states what it changes and keeps up with changes to the default configuration. For example, to analyze at a stricter rule level and enable more of the strict rules: + +```neon +includes: + - phpstan.neon.dist + +parameters: + level: 10 + strictRules: + disallowedLooseComparison: true + overwriteVariablesWithLoop: true +``` + +The baselines and other ignored errors cannot be removed by leaving out an include, but a key ending in `!` replaces the value it would otherwise be merged with. So to see every error, including those the baselines suppress: + +```neon +includes: + - phpstan.neon.dist + +parameters: + ignoreErrors!: [] +``` For more information about configuring PHPStan, see the [PHPStan documentation's Config reference](https://phpstan.org/config-reference). diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index a2f186720f018..be98b5e1503cc 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -1,6 +1,12 @@ # 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. +# This holds what PHPStan needs to know to analyze the WordPress codebase: the extensions for +# WordPress-specific conventions, the paths analyzed and excluded, the supported PHP versions, +# and the constants whose values vary. It rarely changes. +# +# The standards the code is held to, which are the rule level, the baselines, the errors +# ignored as not actionable, and the strict rules enabled, are in phpstan.neon.dist instead, +# so they can be read and reviewed apart from all of this. # # https://phpstan.org/config-reference From ef29125effbcea92fad820d9052c102aedc30a44 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Nov 2026 10:58:56 -0700 Subject: [PATCH 5/6] Build/Test Tools: Note why checkFunctionNameCase is not reset. phpstan.neon.dist resets the parameters phpstan-strict-rules makes stricter, but not checkFunctionNameCase, since tests/phpstan/base.neon already enables it. Say so, so the omission does not look like an oversight. Co-Authored-By: Claude Opus 5.5 (medium effort) Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0 --- phpstan.neon.dist | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 6ce7f45fa7294..cd7d0fd1ef91c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -82,7 +82,8 @@ parameters: numericOperandsInArithmeticOperators: true # Restore the PHPStan defaults of the parameters which phpstan-strict-rules makes stricter, - # since including its rules.neon changes them even when its rules are disabled. + # since including its rules.neon changes them even when its rules are disabled. The exception + # is checkFunctionNameCase, which tests/phpstan/base.neon already enables. # https://phpstan.org/config-reference#stricter-analysis polluteScopeWithLoopInitialAssignments: true polluteScopeWithAlwaysIterableForeach: true From 3e6b51b72f7ae4fd23bad36c7314f9643f36726f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 4 Nov 2026 11:48:20 -0700 Subject: [PATCH 6/6] Build/Test Tools: Register strict rules individually. Including phpstan-strict-rules' rules.neon in phpstan.neon.dist, with only some rules enabled, also made several PHPStan parameters stricter, which then had to be reset one by one. That approach had several problems: * A local phpstan.neon which also included rules.neon, the usual way to opt into the strict rules, failed with "This file is included multiple times". * Setting strictRules.allRules to true locally enabled every rule but none of the stricter parameters, since the reset values took precedence. * reportNonIntStringArrayKey was forced off even with bleeding edge. * A future release making another parameter stricter would silently change the analysis in CI. Instead, register the rules core complies with directly under `rules`, and do not include rules.neon, so no parameters change and the reset list is gone. If a rule class is renamed upstream, PHPStan fails loudly instead. numericOperandsInArithmeticOperators is no longer enabled: it consists of over a dozen rule classes sharing a helper service, and reports nothing at level 5 anyway. Document in the README how to opt into more strict rules by including rules.neon locally, that the rules already registered must then be disabled to avoid duplicate errors, that loop conditions need booleansInLoopConditions without bleeding edge, and that `composer update` is needed after composer.json changes since there is no lock file. Co-Authored-By: Claude Opus 5.5 (medium effort) Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0 --- phpstan.neon.dist | 47 +++++++++++++---------------------------- tests/phpstan/README.md | 31 ++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index cd7d0fd1ef91c..9baee0ab6b3fd 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -16,10 +16,6 @@ includes: # new strict rules. - vendor/phpstan/phpstan-phpunit/extension.neon - # Stricter rules, of which only a subset are enabled by default. See the strictRules - # parameter below. - - vendor/phpstan/phpstan-strict-rules/rules.neon - # Preexisting errors that should be ignored, one baseline per error identifier # so that the remaining work on each is visible as a single shrinking file. # Each is meant to reach zero and be deleted, taking its line below with it. @@ -64,39 +60,26 @@ includes: - tests/phpstan/baselines/variable.undefined.neon # phpstan:baselines end +# Rules from phpstan-strict-rules which core already complies with, or which enforce the +# WordPress Coding Standards. The others are not enabled until the code complies with them, +# but they can be enabled in a local phpstan.neon to work toward that. See +# tests/phpstan/README.md. +# +# These are registered individually, rather than by including phpstan-strict-rules' rules.neon +# with only these enabled, because that file also makes several PHPStan parameters stricter +# even when its rules are disabled. +# https://github.com/phpstan/phpstan-strict-rules +rules: + - PHPStan\Rules\DisallowedConstructs\DisallowedBacktickRule + - PHPStan\Rules\DisallowedConstructs\DisallowedShortTernaryRule + - PHPStan\Rules\Functions\ClosureUsesThisRule + - PHPStan\Rules\Methods\WrongCaseOfInheritedMethodRule + parameters: # https://phpstan.org/user-guide/rule-levels level: 5 reportUnmatchedIgnoredErrors: true - # Rules from phpstan-strict-rules which core already complies with, or which enforce the - # WordPress Coding Standards. All others are disabled until the code complies with them. - # They can be enabled in a local phpstan.neon to work toward that. - # https://github.com/phpstan/phpstan-strict-rules#enabling-rules-one-by-one - strictRules: - allRules: false - disallowedBacktick: true - disallowedShortTernary: true - closureUsesThis: true - matchingInheritedMethodNames: true - numericOperandsInArithmeticOperators: true - - # Restore the PHPStan defaults of the parameters which phpstan-strict-rules makes stricter, - # since including its rules.neon changes them even when its rules are disabled. The exception - # is checkFunctionNameCase, which tests/phpstan/base.neon already enables. - # https://phpstan.org/config-reference#stricter-analysis - polluteScopeWithLoopInitialAssignments: true - polluteScopeWithAlwaysIterableForeach: true - polluteScopeWithBlock: true - checkDynamicProperties: false - checkExplicitMixedMissingReturn: false - checkInternalClassCaseSensitivity: false - reportMaybesInMethodSignatures: false - reportStaticMethodSignatures: false - reportMaybesInPropertyPhpDocTypes: false - reportWrongPhpDocTypeInVarTag: false - checkStrictPrintfPlaceholderTypes: false - reportNonIntStringArrayKey: false # The following ignored errors are not intended to be fixed, as distinct from the baselines # included above. diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index 67fe26178f9b7..e263cd409036d 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -4,7 +4,7 @@ PHPStan is a static analysis tool for PHP that checks your code for errors witho ## Running the tests -PHPStan requires PHP and Composer dependencies to be installed. +PHPStan requires PHP and Composer dependencies to be installed. Since there is no `composer.lock`, run `composer update` after pulling changes to `composer.json`, such as the addition of a PHPStan extension, or PHPStan will fail to load the configuration. 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). @@ -54,9 +54,9 @@ Some errors cannot be ignored, and so cannot be reported as warnings either. The ## The PHPStan configuration -The PHPStan configuration file is located at [`phpstan.neon.dist`](../../phpstan.neon.dist). It sets the standards the code is held to: the rule level, the baselines, the errors ignored as not actionable, and the [strict rules](https://github.com/phpstan/phpstan-strict-rules) enabled. It includes [`base.neon`](base.neon), which configures what PHPStan needs to know to analyze the codebase at all, such as the [WordPress-specific extensions](#wordpress-specific-extensions), the paths analyzed, and the supported PHP versions. +The PHPStan configuration file is located at [`phpstan.neon.dist`](../../phpstan.neon.dist). It sets the standards the code is held to: the rule level, the baselines, the errors ignored as not actionable, and the [strict rules](#strict-rules) enabled. It includes [`base.neon`](base.neon), which configures what PHPStan needs to know to analyze the codebase at all, such as the [WordPress-specific extensions](#wordpress-specific-extensions), the paths analyzed, and the supported PHP versions. -To override the default configuration, create a `phpstan.neon` file in the repository root, which PHPStan reads in place of `phpstan.neon.dist`. Include `phpstan.neon.dist` in it rather than copying it, so the override only states what it changes and keeps up with changes to the default configuration. For example, to analyze at a stricter rule level and enable more of the strict rules: +To override the default configuration, create a `phpstan.neon` file in the repository root, which PHPStan reads in place of `phpstan.neon.dist`. Include `phpstan.neon.dist` in it rather than copying it, so the override only states what it changes and keeps up with changes to the default configuration. For example, to analyze at a stricter rule level: ```neon includes: @@ -64,11 +64,36 @@ includes: parameters: level: 10 +``` + +### Strict rules + +`phpstan.neon.dist` enables only the rules from [phpstan-strict-rules](https://github.com/phpstan/phpstan-strict-rules) which the code already complies with, registering each one individually. It does not include the package's `rules.neon`, since that file also makes several PHPStan parameters stricter, such as `checkDynamicProperties` and `reportMaybesInMethodSignatures`, even when its rules are disabled. + +To work toward complying with more of them, include `rules.neon` in your `phpstan.neon`. It enables every rule and makes those parameters stricter, unless `strictRules.allRules` is set to `false`, in which case only the rules set to `true` are enabled but the parameters are still made stricter. Either way, disable the rules `phpstan.neon.dist` already registers, or each error they report is reported twice: + +```neon +includes: + - phpstan.neon.dist + - vendor/phpstan/phpstan-strict-rules/rules.neon + +parameters: strictRules: + allRules: false disallowedLooseComparison: true overwriteVariablesWithLoop: true + + # Already registered in phpstan.neon.dist. + disallowedBacktick: false + disallowedShortTernary: false + closureUsesThis: false + matchingInheritedMethodNames: false ``` +The rules for `while` and `do`-`while` conditions are only enabled with [bleeding edge](https://phpstan.org/blog/what-is-bleeding-edge) (as are those for unary `+` and `-` operands), so `booleansInConditions: true` alone does not cover loops. Set `booleansInLoopConditions: true` to enable them without it. + +### Seeing every error + The baselines and other ignored errors cannot be removed by leaving out an include, but a key ending in `!` replaces the value it would otherwise be merged with. So to see every error, including those the baselines suppress: ```neon