Bump PHPStan rule level to 1 - #11151
Conversation
Co-authored-by: Dovid Levine <david@axepress.dev>
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the 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. |
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. |
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
e6bd045 to
bf92684
Compare
PHPStan cannot verify that wp_die() always terminates due to its conditional return type and treatPhpDocTypesAsCertain being disabled.
Co-authored-by: Christoph Daum <christoph.daum@coding-pioneers.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
…into bump/phpstan-level-1
Fixes PHPStan issue:
------ ---------------------------------------------------------------
Line wp-admin/includes/class-wp-posts-list-table.php
------ ---------------------------------------------------------------
357 Variable $class in empty() always exists and is always falsy.
🪪 empty.variable
at src/wp-admin/includes/class-wp-posts-list-table.php:357
------ ---------------------------------------------------------------
…edit_link() takes no args
Splitting the baselines by identifier left two things to be done by hand. A newly split out baseline had to be added to the `includes` in phpstan.neon.dist, and one whose identifier had reached zero had to be removed from it. Forgetting the first means the baseline is written but never read, so its errors are reported anyway. Forgetting the second means PHPStan is pointed at a file that is gone. Have `composer phpstan:baselines` do both. The list of baselines now sits between `# phpstan:baselines` markers in the `includes`, and is rewritten after each run to match what is on disk, so the hand written entries around it are never touched. Where the markers are absent they are appended to the end of the block, which is what happens the first time this runs against a configuration. A baseline whose identifier no longer reports anything is also deleted. This previously happened only for an identifier named with --identifier, which left a full run unable to retire anything: the stale file stayed, and its entries were then reported as unmatched ignores. A full run has seen every identifier, so it may retire any file that no longer corresponds to one. Fold tests/phpstan/baseline.php into the split baselines and delete it. It held ten entries, six `empty.variable` and four `isset.variable`, which is to say nothing but the two identifiers that now have their own files. Keeping it would mean a second, hand maintained suppression mechanism alongside the generated ones, and the one place where the exact counts that make these files a tripwire do not apply. Every suppression is now generated, and is now in one place. Verified by regenerating from scratch, which reports no errors and no unmatched ignores. The `includes` rewriting was exercised by removing two entries and adding a stale one, and the retiring by leaving a baseline for an identifier that reports nothing; the run restored both entries, dropped the stale one, and deleted the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README still described a single tests/phpstan/baseline.php, regenerated by passing --generate-baseline to PHPStan directly. Both are now wrong, and following the instructions would undo the split: the file no longer exists, and that flag captures every error a run reports, so it would collect every kind of error into whatever file it was pointed at. Describe what is there instead. Two sections cover how the baselines are organized, one file per error identifier under tests/phpstan/baselines, and how to regenerate them with `composer phpstan:baselines`. The organization section explains why the entries carry a path and an exact count, which is easy to read as incidental detail rather than as the mechanism that makes a new occurrence of an already baselined error report as new. It also spells out the consequence that catches people out: fixing a baselined error means regenerating its baseline in the same change, because the count no longer matches, and the resulting `ignore.count` error cannot itself be ignored. The example entry is copied from the generated baseline rather than written by hand, so it cannot drift into showing a shape that is not produced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The baselines could only be regenerated by calling Composer directly, so anyone working in the Docker environment had no documented way to do it. Add `typecheck:php:baselines` alongside `typecheck:php`, which runs the generator in the php container the same way, and document it first in the README, matching how the analysis itself is documented there. The script ends in `--`. Composer reads anything before that as its own options rather than passing it to the script, and discards what it does not recognize without saying so, so `composer phpstan:baselines --identifier=variable.undefined` quietly regenerates every baseline instead of that one. Supplying the separator in the script means a single `--` is enough from npm, which is what the rest of the README already tells people to write, rather than the two that would otherwise be needed. The README notes the same trap for anyone invoking Composer directly, since there the `--` has to be written by hand and its absence is silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`npm run typecheck:php -- --memory-limit=4G` resolved to `composer phpstan
--memory-limit=4G`, with no `--` for Composer to pass the flag on by. Composer
read it as one of its own, did not recognize it, and discarded it without
saying so, so the analysis ran with the default memory limit. The same went for
every other flag the README documents passing this way, including the file path
that is supposed to narrow the run to a single file.
Composer's handling is easy to confirm:
$ composer phpstan --version
Composer version 2.10.2
$ composer phpstan -- --version
PHPStan - PHP Static Analysis Tool 2.2.5
End the script in `--`, as typecheck:php:baselines already does. A single `--`
from npm is then enough, which is what the README already tells people to write,
and a run with no flags passes a bare separator that PHPStan ignores.
The Composer examples in the README were already correct, since they spell the
separator out. Note there why it is needed, so it does not read as incidental
and get dropped.
env:start and env:composer are left alone. The first passes a flag belonging to
Composer itself, and the second is a passthrough for arbitrary Composer
commands, which a trailing separator would break.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixing a baselined error makes PHPStan report an unmatched ignore, because the baselines record an exact count per file and the entry now describes a state that no longer exists. The fix is to regenerate the baselines, not to change anything in the code, but nothing in the job says so. That is a bad message to leave to inference. The analysis is piped through cs2pr with --errors-as-warnings --graceful-warnings, so the run passes and the only trace is an annotation reading `Ignored error pattern ... was not matched in reported errors` against a line the contributor has just fixed correctly. Read plainly, it looks like the fix is what upset the tool. Detect those reports and fail the job with an explanation instead. The step writes to the job summary, where there is room to say why it happens and that the fix is expected, and adds an error annotation with the command to run. It fails rather than warns because a stale baseline is a repository state problem and not a judgment about the code: merged, it puts the same unmatched ignores in front of everyone else. Detection greps the checkstyle report, which the analysis step now also writes to a file, for the two phrasings PHPStan uses. Verified against a report from a tree where the baselined errors had been fixed, which is the case that matters, and against a report carrying ordinary errors only, which must not trigger it. Also fix the path filter that decides whether this workflow runs at all. It still named tests/phpstan/baseline.php, which no longer exists, and named nothing under tests/phpstan/baselines, so a pull request that only regenerated the baselines would not have run the analysis that checks them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…docs Detection matched two phrasings of the message PHPStan prints for an unmatched ignore. The message is prose, reworded whenever the wording is improved, and nothing fails when it stops matching: the check simply goes quiet and the confusing annotation it exists to explain comes back. The identifier is the stable name for the report, and the checkstyle format carries it in the `source` attribute, so match on that instead. Matching the `ignore.` prefix rather than the two identifiers seen so far also covers any later addition, since every report in that group means the same thing, that the ignore configuration no longer describes the code. Say what to do when the report names an `@phpstan-ignore` annotation rather than a baseline entry, which is the one case where regenerating does not help and the annotation has to be removed by hand. Link the README rather than naming its path. The job summary is Markdown, so it can carry a real link, and the annotation can carry the URL. Both are built from the commit being tested, so the documentation shown is the documentation that came with the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both guards in the step read as unexplained. The first tests for a file whose absence is not obviously possible, and the second matches a string prefix that names no identifier a reader can look up, so neither says why the step would stop there. Give each one a comment. The first notes that the step runs even when the analysis before it failed, so the report may never have been written, and that failing on its absence would only obscure the real failure. The second names `ignore.unmatched` and `ignore.count` as what the prefix catches, and says why the prefix is matched rather than the two names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An analysis that crashed passed as a green run. The report is piped through cs2pr, which exits successfully so that reported errors annotate a pull request without failing it, and a pipeline reports only the status of its last command. The status of the analysis itself was therefore discarded, and it was the only sign that anything had gone wrong. An out of memory kill or a parse error in the configuration looked exactly like a clean run. Recover that status from PIPESTATUS and act on it. PHPStan exits 1 when it has errors to report, which is the expected case here and is what the annotations are for, so only a status above that is treated as a failure to finish. Setting pipefail would not work, since it would fail the run for reported errors too, which is the case cs2pr is configured to tolerate. Verified against stubbed exit statuses: 0 and 1 pass through, while 2, 137 and 255 emit an annotation and fail the step. Also say in phpstan.neon.dist what its ignoreErrors are for, now that the baselines it includes hold the same kind of entry with the opposite intent. A baseline records work still to be done and is meant to reach zero and be deleted; an entry in the configuration is a decision that the code is right as written. Without that written down, the two are indistinguishable, and the easiest way to quiet a report is to add it to whichever list is closer to hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
actionlint failed the workflow lint. The job summary was built from a run of echo calls, and the ones carrying Markdown code spans put backticks inside single quotes, which shellcheck reports as SC2016: a command substitution that will not expand. The backticks are meant literally, so the report is a false positive, but it is one the linter is right to make about that shape of code. Write the summary as a quoted heredoc instead. Nothing in it is expanded, so the backticks are no longer the shell's concern, and the Markdown reads as Markdown rather than as thirty quoted fragments. The one line needing a variable is appended after it, and has no backticks to quote. Verified by extracting both `run` blocks from the workflow and running shellcheck over them at the severity actionlint reports at, which is clean, and by running the step against a real checkstyle report to confirm the summary still renders with its fences and code spans intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Oh, nevermind, it's right here in the Summary: https://github.com/WordPress/wordpress-develop/actions/runs/30981363599
|
The summary was written wrapped at the width of the workflow file, which is not the width it is read at. A newline renders as a line break there rather than as a space, so the paragraphs kept those breaks and read as ragged columns down the left of a much wider panel. Put each paragraph on one line and let the rendered summary wrap to its own width. Link `tests/phpstan/baselines` as well, since the summary is telling the reader about a directory they may never have opened, and it was already linking the README beside it. Both links are written in reference style. The URLs are the only part of the summary that needs a variable, and the heredoc holding it is quoted so that its code spans and fences stay literal, so defining the URLs after it keeps all of the prose inside the one block rather than splitting it around the two lines that interpolate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This reverts commit 8ebe862.
Level 1 adds detection of possibly undefined variables, and of unknown magic methods and properties on classes with `__call` and `__get`. The 494 errors this surfaces in existing code are recorded in baselines rather than being fixed here, so that new code is held to level 1 straight away while the existing reports are worked through separately. No files under `src` are changed. The `tests/phpstan/baseline.php` file is replaced by one baseline per error identifier under `tests/phpstan/baselines`, so that the remaining work on each kind of error is visible as a single file that should shrink to nothing and then be deleted. Every entry is scoped to the file which the error occurs in and carries an exact occurrence count, so that a new occurrence of an already baselined error is reported rather than absorbed. The consequence is that fixing a baselined error means regenerating its baseline in the same change, because the count no longer matches. PHPStan's own `--generate-baseline` captures every error a run reports, with no way to restrict it to one identifier, so `tests/phpstan/generate-baselines.php` is added to write the files instead, exposed as `composer phpstan:baselines` and as `npm run typecheck:php:baselines`. A run also deletes any baseline whose identifier no longer reports anything, and rewrites the list of baselines in `phpstan.neon.dist`. The `ignoreErrors` in that file now has a comment explaining how it is distinct from a baseline: an entry there is a decision that the code is right as written, whereas a baseline entry is work still to be done. The constants that `add_theme_support()` defines are declared in the configuration so that the errors around them are resolved rather than recorded, and `tests/phpstan/README.md` is updated throughout. Three problems in the static analysis GHA workflow are fixed as well. Fixing a baselined error makes PHPStan report an unmatched ignore, which surfaced only as an annotation reading like a complaint about a correct fix; the job now detects any `ignore.*` report and fails with an explanation of what to run. An analysis that did not finish passed as a green run, because the status of the pipeline was that of `cs2pr` rather than of PHPStan; that status is now recovered and a run that did not finish fails. The path filter deciding whether the workflow runs named only the old baseline file, so a pull request that merely regenerated the baselines would not have run the analysis that checks them. Developed in #11151. Follow-up to r61699. Props westonruter, sabernhardt, apermo, johnjamesjacoby, adamsilverstein, justlevine. See #61175. Fixes #64680. git-svn-id: https://develop.svn.wordpress.org/trunk@63019 602fd350-edb4-49c9-b593-d223f7449a82
Level 1 adds detection of possibly undefined variables, and of unknown magic methods and properties on classes with `__call` and `__get`. The 494 errors this surfaces in existing code are recorded in baselines rather than being fixed here, so that new code is held to level 1 straight away while the existing reports are worked through separately. No files under `src` are changed. The `tests/phpstan/baseline.php` file is replaced by one baseline per error identifier under `tests/phpstan/baselines`, so that the remaining work on each kind of error is visible as a single file that should shrink to nothing and then be deleted. Every entry is scoped to the file which the error occurs in and carries an exact occurrence count, so that a new occurrence of an already baselined error is reported rather than absorbed. The consequence is that fixing a baselined error means regenerating its baseline in the same change, because the count no longer matches. PHPStan's own `--generate-baseline` captures every error a run reports, with no way to restrict it to one identifier, so `tests/phpstan/generate-baselines.php` is added to write the files instead, exposed as `composer phpstan:baselines` and as `npm run typecheck:php:baselines`. A run also deletes any baseline whose identifier no longer reports anything, and rewrites the list of baselines in `phpstan.neon.dist`. The `ignoreErrors` in that file now has a comment explaining how it is distinct from a baseline: an entry there is a decision that the code is right as written, whereas a baseline entry is work still to be done. The constants that `add_theme_support()` defines are declared in the configuration so that the errors around them are resolved rather than recorded, and `tests/phpstan/README.md` is updated throughout. Three problems in the static analysis GHA workflow are fixed as well. Fixing a baselined error makes PHPStan report an unmatched ignore, which surfaced only as an annotation reading like a complaint about a correct fix; the job now detects any `ignore.*` report and fails with an explanation of what to run. An analysis that did not finish passed as a green run, because the status of the pipeline was that of `cs2pr` rather than of PHPStan; that status is now recovered and a run that did not finish fails. The path filter deciding whether the workflow runs named only the old baseline file, so a pull request that merely regenerated the baselines would not have run the analysis that checks them. Developed in WordPress/wordpress-develop#11151. Follow-up to r61699. Props westonruter, sabernhardt, apermo, johnjamesjacoby, adamsilverstein, justlevine. See #61175. Fixes #64680. Built from https://develop.svn.wordpress.org/trunk@63019 git-svn-id: http://core.svn.wordpress.org/trunk@62238 1a063a9b-81f0-0310-95a4-ce76da25c4cd
|
Bump to rule level 2: #12852 |







Raises the PHPStan rule level from 0 to 1, which adds detection of:
The errors that level 1 surfaces in existing code are recorded in baselines rather than fixed here, so that new code is held to level 1 straight away while the existing reports are worked through separately. No
src/files are changed. Almost all of the diff is that baseline and the tooling that maintains it.Baselines split by error identifier
tests/phpstan/baseline.php, which is empty on trunk, is replaced by one file per error identifier undertests/phpstan/baselines/:variable.undefined.neonempty.variable.neonisset.variable.neonSplitting them this way keeps each kind of error visible as a single file that should shrink to nothing and then be deleted, rather than as part of one large file in which every kind is mixed together. Progress on any one of them is legible from the file's size.
Every entry is scoped to the file the error occurs in and carries an exact occurrence count:
Both the path and the count matter. A new occurrence of an already baselined error does not match the entry, even in a file that is already listed, and is reported as a new error. That is the point of recording them this way: the baselines describe exactly what exists today, so nothing new slips in behind them.
The consequence is that fixing a baselined error means regenerating its baseline as part of the same change, because the count no longer matches. PHPStan reports that as
ignore.unmatchedorignore.count, neither of which it allows to be ignored or baselined.composer phpstan:baselinesPHPStan's own
--generate-baselinecaptures every error a run reports, with no way to restrict it to a single identifier, so it cannot refresh one of these files without sweeping every other kind of error into it.tests/phpstan/generate-baselines.phpis added to do that instead, exposed as a Composer script and asnpm run typecheck:php:baselinesfor the Docker environment.A run:
--identifier;# phpstan:baselinesmarkers in theincludesofphpstan.neon.dist, so that neither adding a newly split out baseline nor retiring a finished one needs a manual edit;--combined.It works by running the analysis with the existing baselines suppressed, generating an intermediate baseline in PHPStan's PHP format, and reading it back with
require, so nothing has to parse or re-escape the message patterns. Regenerating the committed baselines reproduces them byte for byte, which is how the NEON it writes was checked against what PHPStan writes itself.Configuration
phpstan.neon.distis at level 1, includes the baselines, and its ownignoreErrorsnow says what that list is for as distinct from a baseline: an entry there is a decision that the code is right as written, whereas a baseline entry is work still to be done.add_theme_support()defines are declared intests/phpstan/base.neonandtests/phpstan/bootstrap.php, so that theconstant.notFounderrors around them are resolved rather than baselined.Continuous integration
Fixing a baselined error makes PHPStan report an unmatched ignore, and nothing in the job said so. Because the analysis is piped through
cs2pr --errors-as-warnings --graceful-warnings, that surfaced as a passing run whose only trace was an annotation readingIgnored error pattern ... was not matched in reported errorsagainst a line the contributor had just fixed correctly.The workflow now detects any
ignore.*report and fails with an explanation, in an annotation and in the job summary, saying that this is expected when an error is fixed and giving the command to run.Two further problems in the same workflow are fixed:
cs2prexits successfully, and a pipeline reports only the status of its last command, so an out of memory kill or a broken configuration was indistinguishable from a clean run. The status of the analysis is now recovered and a run that did not finish fails.tests/phpstan/baseline.phpand nothing undertests/phpstan/baselines, so a pull request that only regenerated the baselines would not have run the analysis that checks them.Documentation
tests/phpstan/README.mdis updated for all of the above: how the baselines are organized, why the entries carry a path and an exact count, what that implies when fixing one, and how to regenerate them.Deferred to a follow-up
An earlier revision of this branch fixed level 1 errors across ten files in
src/wp-adminandsrc/wp-includes. Those fixes were reverted in cce3ac0 so that this pull request is limited to the level bump and the tooling around it, and they will be proposed separately. The errors they addressed are among those baselined above.For reference, the tally when this pull request was opened, before the baselines were split by identifier:
empty.variableempty()on variables that always exist, are always falsy, or are not falsyconstant.notFoundisset.variableisset()on variables that always exist, are not nullable, or are never definedvariable.undefinedarguments.countconstructor.unusedParameterAnd after the fixes that have since been reverted:
empty.variableempty()on variables that always exist, are always falsy, or are not falsyisset.variableisset()on variables that always exist, are not nullable, or are never definedThose counts covered only the errors then in
tests/phpstan/baseline.php. The 494 above is the full level 1 tally acrosssrc/, which is larger becausevariable.undefinedwas previously suppressed wholesale by name rather than recorded per file.This cherry-picks some commits from #11009.
See also #11037 which bumps to level 4.
Trac ticket: https://core.trac.wordpress.org/ticket/64680
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: The baseline generator, the workflow changes, the README, and most of the commit messages and this description were drafted by the model, along with the analysis of each individual PHPStan error. Every change was directed, reviewed and revised by me, and the verification of each claim made here was carried out and checked in the working tree.
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.