Repository navigation
Build/Test Tools: Add phpstan-strict-rules and enable rules core complies with - #15399
westonruter wants to merge 7 commits into
Conversation
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) <noreply@anthropic.com> Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
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) <noreply@anthropic.com> Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0
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) <noreply@anthropic.com> Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. Props Bot commandsYou can trigger Props Bot actions by commenting on this PR:
|
Resolve the conflict in HashNotationVisitor::split_tags() by keeping the explicit false check in place of the short ternary, together with trunk's simplified docblock furniture regex. Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com> Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0
|
@apermo @szepeviktor Please review. Seems uncontroversial. |
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) <noreply@anthropic.com> Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0
There was a problem hiding this comment.
🟡 Changes recommended
The configuration still omits checkFunctionNameCase from its reset list, leaving strict function-name case checking enabled and potentially introducing errors into the default analysis.
1 open issue
What changed in this PR
Adds phpstan/phpstan-strict-rules 2.1.1 to WordPress core’s development tooling, enables compatible strict rules, and documents local PHPStan configuration overrides.
Changes:
- Adds the pinned strict-rules Composer dependency.
- Enables five compatible rules while restoring selected PHPStan defaults.
- Replaces the remaining short ternary with an explicit
falsecheck. - Updates PHPStan configuration documentation.
phpstan.neon.distshould also resetcheckFunctionNameCaseto prevent unintended errors in the default analysis.
| File | Summary |
|---|---|
tests/phpstan/README.md |
Documents the configuration structure, local overrides, strict-rule opt-ins, and removing ignored errors. |
tests/phpstan/HashNotationVisitor.php |
Replaces the short-ternary fallback with an explicit false check. |
tests/phpstan/base.neon |
Clarifies the separation between analysis setup and project standards. |
phpstan.neon.dist |
Includes strict rules, enables the supported subset, and restores PHPStan defaults; checkFunctionNameCase remains to be reset. |
composer.json |
Adds the pinned phpstan/phpstan-strict-rules development dependency. |
This pull request requires changes.
Review effort: Lite
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) <noreply@anthropic.com> Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0
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) <noreply@anthropic.com> Claude-Code-Session-Id: a06fee1f-3af9-4708-8113-35e373c5f1f0
This adds `phpstan/phpstan-strict-rules` as a development dependency so that its rules can be enabled in a local `phpstan.neon` to work toward compliance, and it enables by default the rules which core already complies with or which enforce the WordPress Coding Standards: * Disallow the backtick operator. * Disallow short ternaries (the one remaining instance is replaced). * Disallow `$this` in closures. * Require inherited method names to match in case. The rules are registered individually rather than by including the package's `rules.neon`, because that file also makes several PHPStan parameters stricter even when its rules are disabled, and because a local config including it would otherwise fail on the duplicate include. The README documents how to opt into the other rules locally, and the roles of `base.neon` and `phpstan.neon.dist` are clarified so that local configs include the latter rather than copying it. Developed in #15399. Follow-up to r61699, r63420. Props westonruter, apermo. See #65817. git-svn-id: https://develop.svn.wordpress.org/trunk@65265 602fd350-edb4-49c9-b593-d223f7449a82
This adds `phpstan/phpstan-strict-rules` as a development dependency so that its rules can be enabled in a local `phpstan.neon` to work toward compliance, and it enables by default the rules which core already complies with or which enforce the WordPress Coding Standards: * Disallow the backtick operator. * Disallow short ternaries (the one remaining instance is replaced). * Disallow `$this` in closures. * Require inherited method names to match in case. The rules are registered individually rather than by including the package's `rules.neon`, because that file also makes several PHPStan parameters stricter even when its rules are disabled, and because a local config including it would otherwise fail on the duplicate include. The README documents how to opt into the other rules locally, and the roles of `base.neon` and `phpstan.neon.dist` are clarified so that local configs include the latter rather than copying it. Developed in WordPress/wordpress-develop#15399. Follow-up to r61699, r63420. Props westonruter, apermo. See #65817. Built from https://develop.svn.wordpress.org/trunk@65265 git-svn-id: http://core.svn.wordpress.org/trunk@64414 1a063a9b-81f0-0310-95a4-ce76da25c4cd

✅ Committed in r65265 (985cab8).
This adds
phpstan/phpstan-strict-rules2.1.1 torequire-dev, pinned exactly likephpstan/phpstanandphpstan/phpstan-phpunit.phpstan.neon.distnow registers the four rules which core already complies with (Tier 1 below) underrules:DisallowedBacktickRuleandDisallowedShortTernaryRule, since the WordPress Coding Standards prohibit both constructs outright.ClosureUsesThisRuleandWrongCaseOfInheritedMethodRule, as guards against regressions.The one short ternary in core, in
tests/phpstan/HashNotationVisitor.php, is replaced with an explicitfalsecheck. With that, the default analysis reports no new errors.The rules are registered individually rather than by including the package's
rules.neonwithstrictRules.allRules: false, which was this PR's first approach.rules.neonalso makes several PHPStan parameters stricter regardless ofallRules, which would add about 300 errors (see the last section below), so those parameters then had to be reset one by one inphpstan.neon.dist. That had several problems:phpstan.neonthat also includedrules.neon, the usual way to opt into the strict rules, failed with "This file is included multiple times".allRules: truelocally enabled every rule but none of the stricter parameters, since the reset values took precedence.reportNonIntStringArrayKeywas forced off even with bleeding edge, whererules.neonwould enable it.Contributors can opt into more rules from a local
phpstan.neonwhile the codebase works toward them, by includingrules.neonas usual. The rulesphpstan.neon.distalready registers should then be disabled, or each error they report is reported twice:All of this was verified with a fixture file violating each of the four rules: the dist config reports each violation once, and the local config above also reports each once, while without the four
falselines each is reported twice.2.1.x requires PHPStan 2.3.0+ (core is at 2.3.4) and PHP 7.4+. The 2.1.1 release also reports implicit
mixedin boolean conditions at level 10, which is useful for anyone running a local level 10 override.Which rules could be enabled by default?
To ground this in data, PHPStan was run with the
phpstan.neon.distconfiguration from trunk (level 5, all baselines) plusrules.neonwith every rule enabled, and the new errors were tallied by identifier. That run reports 9,372 errors. Each rule is assessed below against the WordPress PHP Coding Standards and core conventions. Counts will be higher at stricter levels, since several rules report more oncemixedis checked.Tier 1: Enabled in this PR (zero or near-zero errors)
disallowedBacktickGeneric.PHP.BacktickOperator.disallowedShortTernaryUniversal.Operators.DisallowShortTernary. The one hit, intests/phpstan/HashNotationVisitor.php(PHPStan tooling, not shipped code), is fixed in this PR.closureUsesThismatchingInheritedMethodNamesThe backtick and short ternary rules duplicate PHPCS, so they mostly serve as a backstop (e.g. for files PHPCS excludes). The rest are free guards against regressions.
numericOperandsInArithmeticOperatorsalso reports 0 at level 5 and was enabled in an earlier revision of this PR, but it is not one rule: it is over a dozen operator rule classes sharing a helper service, which is verbose to register by hand for a rule that reports nothing at the default level. It can be added when the level is raised.booleansInLoopConditionsreported 0 in the initial run only because it is gated on bleedingEdge, which the dist config does not enable. Setting it totruebypasses that gate and reports 32while/do-whileerrors, so it is left disabled. It belongs withbooleansInConditionsin Tier 3.Tier 2: Fix (or justify) a small number of errors, then enable
disallowedLooseComparisonUniversal.Operators.StrictComparisonsas a warning, which is how these survived. 20 are inWP_Query, where query vars may be strings or ints, so each needs care; the "absolutely necessary" ones can carry an inline ignore with a reason.disallowedImplicitArrayCreation$menu/$submenuinwp-admin/menu.php, but others (e.g.$fieldsinWP_Debug_Data,$role_queriesinWP_User_Query) are genuine "variable does not exist" cases.dynamicCallOnStaticMethod$this->static_method(). 15 areImagick::getResourceLimit()inWP_Debug_Data. Mechanical fix:static::, which also preserves late static binding for subclasses (e.g.WP_Image_Editor::get_extension()).switchConditionsMatchingTypetwentyelevenandtwentynineteen(int vs. string/float case labels), andoption.php(nullswitch condition against'on'/'yes'cases), which deserves a closer look.overwriteVariablesWithLoopforeachvalue variable clobbering a variable still in use afterwards, which is a real bug pattern (e.g.$revisioninwp-admin/includes/revision.php,$nodein the HTML Processor).strictFunctionCallsbase64_decode()without$strict. (in_array()without$strictis already prohibited byWordPress.PHP.StrictInArray.) Note that addingtruechanges behavior on malformed input, so each call needs review rather than a blanket change.illegalConstructorMethodCallWP_Widgetconstructor shims, plus re-invoking__construct()onWP_Meta_Query/WP_Theme. These exist for backward compatibility, so they are likely ignore-with-reason rather than fixes.requireParentConstructorCallWalker_Nav_Menu_Checklist,_WP_List_Table_Compat,wp_xmlrpc_server, andWP_REST_Autosaves_Controller. Each likely deliberate, but worth documenting.uselessCastbase.neonsetstreatPhpDocTypesAsCertain: false, a cast is only reported when the native type already matches, e.g.$post_categoryin_wp_ajax_add_hierarchical_term()is cast with(array)when assigned and then again when passed toarray_map(). So these are redundant, not hardening against PHPDoc types that plugins may violate. The exception is the 9 in output, likeecho (int) $post_id;, since the cast is what satisfiesWordPress.Security.EscapeOutput; those need an inline ignore or another escaping function.Each of these could be enabled with a new per-identifier baseline (following the existing one-baseline-per-identifier approach), so new code is held to the rule while the existing errors are worked down under this ticket.
Tier 3: Not recommended as defaults
disallowedEmpty! empty()as the preferred idiom ("An exception would be using! empty()…").booleansInConditionsif: 1,212,!: 1,281,&&: 751,||: 157, ternary: 246,elseif: 53. Truthiness checks are pervasive and idiomatic in core, and WPCS has no such rule. A good opt-in for new code locally, especially at level 10.noVariableVariables$post->$fieldand$this->$method(), which core APIs depend on. Actual$$varvariable variables: 0.strictArrayFilterarray_filter( $array )without a callback is a common core idiom. Its loose semantics drop'0', so some call sites may be latent bugs worth auditing, but making this a default would be churn.Side effects of including
rules.neonat allEven with
strictRules.allRules: false,rules.neonchanges the defaults of several PHPStan parameters. Under the dist config these add:reportMaybesInMethodSignaturesmethod.childParameterType,method.childReturnTypecheckDynamicPropertiesproperty.notFoundreportMaybesInPropertyPhpDocTypesproperty.phpDocTypecheckInternalClassCaseSensitivityclass.nameCaseimagickvs.Imagick)polluteScopeWithLoopInitialAssignments, etc.variable.undefinedreportWrongPhpDocTypeInVarTagvarTag.typecheckExplicitMixedMissingReturnreturn.missingSince
phpstan.neon.distdoes not includerules.neon, none of these apply by default. Any of them could be enabled individually in follow-up PRs, with baselines, as with the Tier 2 rules.The method signature variance errors are the largest group. PHPStan always reports an override whose types are definitely incompatible with the parent's;
reportMaybesInMethodSignaturesalso reports ones that are only maybe compatible. For example,Walker::start_el()documents$data_objectasobject, whileWalker_Category_Checklist::start_el()narrows it toWP_Term, which is not contravariant. Similarly,Language_Pack_Upgrader::clear_destination()returnsbool|WP_Errorwhere the parentWP_Upgrader::clear_destination()returnsWP_Error|true, which is not covariant. These are PHPDoc types, not native type declarations (whose compatibility PHP enforces itself), so fixing them means correcting docblocks, e.g. widening a parent@returnor adding a@templatetoWalkerfor its data object type. That does not touch the native signatures plugins and themes override, so this cluster of about 190 errors is a reasonable Tier 2 candidate.Documentation of the PHPStan configuration files
This also updates how the two configuration files, and overriding them, are documented.
tests/phpstan/base.neonsaid it was "kept separate from the main PHPStan configuration file to allow for easy overloading while baseline errors are being fixed." The idea was that a local config could includebase.neonalone, to get the WordPress-specific setup without the baselines, since a config that includesphpstan.neon.distcannot remove its includes. That is no longer necessary: NEON's!suffix replaces a value instead of merging into it, so a localphpstan.neonthat includesphpstan.neon.distand setsignoreErrors!: []reports every error the baselines suppress (verified onsrc/wp-admin/admin.php, which then reports thevariable.undefinederrors frombaselines/variable.undefined.neon).Merging
base.neonintophpstan.neon.distwas considered, but the split is still useful for a different reason.base.neon(~360 lines) is what PHPStan needs to know to analyze the codebase at all, and rarely changes: the WordPress-specific extensions, the paths analyzed and excluded, the supported PHP versions, anddynamicConstantNames.phpstan.neon.distis the standards the code is held to: the level, the baselines, the errors ignored as not actionable, and now the strict rules. Keeping them apart keeps the latter short enough to review. Merging would also mean rewriting every../../srcpath, updating references in the CI workflow, README,bootstrap.php, several extension classes, andgenerate-baselines.php, and breaking any local config that includesbase.neondirectly.So the documentation changes are:
base.neonheader now describes it by what it contains, and points tophpstan.neon.distfor the standards.phpstan.neon.distheader and the README said to copyphpstan.neon.disttophpstan.neon, which leaves the copy to go stale. They now recommend including it.phpstan.neonwhich raises the level, and of one which usesignoreErrors!: []to show every error.while/do-whilecondition rules are only enabled with bleeding edge unlessbooleansInLoopConditionsis set, sobooleansInConditions: truealone does not cover loops.composer.lock,composer updateis needed after pulling a change like this one, or PHPStan fails to load the configuration.Trac ticket: https://core.trac.wordpress.org/ticket/65817
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5 (medium effort)
Used for: Adding the dependency, enabling the Tier 1 rules, fixing the short ternary, updating the configuration documentation, running PHPStan with the strict rules against core, tallying the errors by identifier, and drafting the analysis against the WordPress Coding Standards; reviewed by me.
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.