[wp-trac] [WordPress Trac] #61175: Integrate PHPStan into the core development workflow
WordPress Trac
noreply at wordpress.org
Wed Sep 3 13:35:47 UTC 2025
#61175: Integrate PHPStan into the core development workflow
--------------------------------------+-------------------------
Reporter: westonruter | Owner: justlevine
Type: enhancement | Status: assigned
Priority: normal | Milestone: 6.9
Component: General | Version:
Severity: normal | Resolution:
Keywords: has-patch has-unit-tests | Focuses:
--------------------------------------+-------------------------
Comment (by justlevine):
looking forward @dmsnell - one of these days I hope to finally make it to
a WordCamp (if only I'd known I was choosing pheonix mid-August over
portland 🌞)
>> adopting PHPStan as a linting tool means we need auto
-accept/-reject/-anything…When faced with a PHPStan error, it can be
triaged by
>
> I’m not saying it means we have to, but I will be surprised if we do
this and people don’t implicitly choose to. [...] My views on this are
definitely influenced by my experience with WPCS and some other systems
like code coverage requirements. I’ve seen it backfire and am definitely
at risk of overreacting, but given that we’re discussing adding a CI step
to reject changes that we haven’t used broadly yet, I think we can take
steps to mitigate the risk.
I think we're in agreement (and I don't think it's an overreaction).
Between learning from the experience of adopting W(/PH)PCS and some of the
particulars *of* PHPStan (IIRC their concept of a central "baseline" and
other config choices is also in response to similar unintended side-
effects, I feel pretty confident that between docs and maybe some changes
to how those CI communications those changes we can ensure that there's no
reason/incentive to e.g. write code for the tool, and that it **reduces**
friction for contributors/committers instead of adding to it.
----
>> PHPStan/the CI surfaces _new_ errors
>
> I’m guessing this includes a percentage of changes that touch legacy
code and make the legacy code better in some ways without solving all
potential problems.
Indeed it does. Solely illustrative example: say, we've got a legacy class
that's using a PHP8 breaking `is_callable( [
$my_class_name::class,'non_static_method'] )` 5 times, and someone updates
code to add a new one.
PHPStan lets us:
- ignore the file entirely (without touching the code)
- ignore the specific error type (or message, or type + message combo) for
that file (without touching...)
- baseline the 5 existing occurrences (or combo baseline/ignore if there
was justification? idk an intentional shim maybe?), but still show an
error about the 6th (without touching...)
- manually baseline *or* ignore the 6th error, independently of the how
the existing 5 are handled (by updating the config but still w.o the
code).
- and _then_ the usual ignore/disable line/rule exit hatch's that should
only be a last resort i.e. the rare "true" false-positive.
All independently of any other PHPStan errors found in that legacy class
(like how `$my_class_name` isn't defined anywhere because the old code
uses `$my_CLASs_Name` for "legacy compatibility" ).
True, the IRL success depends on our ability to accurately translate WPCS
into PHPStan rulesets (and where the proposed PR needs the **most help**)
but since we can adopt it piecemeal (via config/baselines) and iterate on
it without disrupting the codebase, the risk here is really low and easy
to rectify if we don't get it perfectly tuned in the initial commit.
> Those baselines are kind of gnarly.
Yupppp and thank you, I've been splitting them by-level manually for the
sake of review (instructions in the PR description) since as noted seeing
what sort of errors are caught is key to help us align the rules for WPCS
so we're not needlessly bothered by false positives. If people like that
approach I can bash-script it, but most projects treat it like any other
auto-generatable (but *diff`able*) tool file e.g. `package-lock.json`,
since once committed you only care to see if anything is being
unintentionally added.
> What happens though when PHPStan updates
PHPStan follows a strict SemVer definition, where changes to
flags/rules/smells only happen in major releases, where the upgrade path
would be to update the relevant configs + autoregenerate the baselines.
(Theoretically possible they'd choose to rename code-annotations the way
PHPUnit did, but considering they're meant as a last result even if there
wasn't a b/c migration path the disruptions would be minimal)
In minor/patches it's possible that a bug fix or improvement previously
hidden errors to be detected or remediated, (e.g. now that it recognizes
the array shape, it stops warning you to check if the `array_key_exists()`
before accessing and instead that `$arg['permission']` is really
`$args['has_permissions']` and expects a `callable<bool>` instead of
`true|\WP_Error` ).
Assumedly we'd pin a specific version number at merge to avoid that
happening unintentionally.
> Does PHPStan report on which baseline ignores end up matching no lines
of code?
Indeed, there's the [`reportUnmatchedIgnoredErrors` config
value](https://phpstan.org/user-guide/ignoring-errors#reporting-unused-
ignores) that lets us decide whether unmatched ignored errors should be
treated as an error itself, including the granular counts/file where the
error was previously.
Currently, the unmatched baseline rules *does* error, but mainly way
mainly so I_could easily track and illustrate for review the tech debt
changes on the PR (and remediation branch), but I've been hesitant whether
that's the right approach for a core commit, since even though tech debt
remediation should be intentional/auditable if that's not the **goal** of
the PR, we don't want it getting the way (to avoid new tech debt slipping
in by an autogenerate, writing code to appease the tool, lockfile-style
merge conflicts, etc etc)...
I *was* leaning towards recommending that regenerating the baseline as a
new 1-command step in the release process, but:
> It would be great if exclude rules which never trigger could be
automatically removed.
Riffing off this, we could regenerate-baselines with CI on merge (in
theory, I don't fully get the svn <=> gh mechanics). A core commit means
everything there has been reviewed/approved and therefore justifies a new
baseline of "existing tech debt". But that means increased potential for
merge conflicts, albeit a trivial one 🤔
----
> I’m still most curious about running the proposed rules against every
commit from the past few years and see how many issues would have been
raised in those commits had we been running this code.
Like minds 😇
Lazy version alternative is the history at https://github.com/WordPress
/wordpress-develop/pull/7619/commits.
Each of those `tests: regenerate PHPStan baselines after rebase` commits
is the snapshot of changes from PRs merged into core E.g.
https://github.com/WordPress/wordpress-
develop/pull/7619/commits/9c8f09656b9bbf2f4f7173328074c75cf2d63581
(rebasing loses the commit date fidelity of when specifically over the
last year did the resync happen, I've been averaging a rebase ~3-4 weeks).
Mind you, the results are a bit juiced with the in-parallel remediations
merged via #52217 #63268 , but in a way I think that kinda proves the
hypothesis here: it's "so" nondisruptive that parallel development, active
remediation, even major config changes can happen in complete, and the
diff is still just a single (or split by level current) autogenerated file
from a single `--generate-baseline` command
----
@dmsnell if you have the time I'd love to connect and walk you through the
specific baselines/errors/behaviors of in the current PR, probably the
most efficient way to tweak the setup so it's not just
theoretically/philosophically sound but practically robust IRL.
(Offer's open to any other contributor/committer looking to give feedback!
Slack DM's open)
--
Ticket URL: <https://core.trac.wordpress.org/ticket/61175#comment:45>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list