Skip to content

PHPStan: Update to 2.3.5 and recommend --fix for the baseline - #86333

Open
westonruter wants to merge 6 commits into
trunkfrom
update/phpstan-2.3.5
Open

westonruter wants to merge 6 commits into
trunkfrom
update/phpstan-2.3.5

Conversation

@westonruter

@westonruter westonruter commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

What?

Follow up to #66693. Companion to WordPress/wordpress-develop#15479, which does the same for core.

Updates PHPStan from 2.3.4 to 2.3.5, and adds composer phpstan:fix (and npm run typecheck:php:fix), which uses its new --fix option to remove out-of-date baseline entries rather than regenerating the baseline.

Why?

PHPStan 2.3.5 marks the errors it can fix itself with 🔧, and these include stale ignores. --fix lowers a baseline count that is now too high, removes an entry that matches nothing, and removes an @phpstan-ignore comment that no longer ignores anything.

That suits Gutenberg better than regenerating. composer phpstan:baselines is PHPStan's --generate-baseline, which records every error the run reports, so the job summary had to warn against regenerating while other errors were still reported. --fix never adds an entry, so it is safe to run then too.

Running it as composer phpstan -- --fix is easy to get wrong, though: leaving out the -- makes Composer discard the flag silently, so the analysis just reports the stale entries again. And with --fix, PHPStan reports only what it fixed, and exits with 0 when every fixable error was fixed, even when other errors remain. A dedicated script avoids both.

How?

  • Bumps phpstan/phpstan to 2.3.5. The analysis reports no errors with it and no baseline entry goes stale, so the baseline is unchanged. phpstan-phpunit, extension-installer, and phpstan-wordpress are already at their latest versions. The WP_REST_Request offset ignores stay, since Resolve offsetSet() template types for ArrayAccess offset writes phpstan/phpstan-src#6903 is not merged yet.

  • Adds composer phpstan:fix, which chains two steps, the second run only when the first succeeds:

    1. phpstan analyse --configuration=phpstan.neon.dist --fix, which updates the baseline in place.
    2. phpstan analyse --configuration=phpstan.neon.dist, since --fix reports only what it fixed. It reuses the result cache, so it is quick, and it is what makes the script fail when errors remain.

    Both analyses name phpstan.neon.dist, whatever a local phpstan.neon says. In core, running --fix with a local configuration at level 10 with bleeding edge removed entries the default configuration still needs, because that configuration reports those errors with different messages. When nothing is fixable, PHPStan exits with 1 and stops the chain after the first step, which the coding guidelines note. npm run typecheck:php:fix runs the script in the local environment.

  • The coding guidelines, tools/phpstan/README.md, phpstan.neon.dist, and the annotation and job summary written by report.ts now point to composer phpstan:fix (or npm run typecheck:php:fix). The guidelines note that --fix has to be a run over all the paths, since PHPStan reports no out-of-date entries when it analyzes only some files.

  • report.ts reads the fixable flag PHPStan's JSON report now carries. A fixable error's annotation says how to fix it, and the job summary marks it with 🔧, as PHPStan's table output does.

Not adopted: registering tools/phpstan/src/ services with PHPStan's new attributes, since that only works when PHPStan runs on PHP 8.0+ and Gutenberg supports 7.4.

Unlike core, which keeps one baseline per identifier and gains a --prune option there for deleting a baseline that --fix empties, Gutenberg has a single baseline, so no pruning step is needed.

Testing Instructions

  1. Run composer update, then npm run build and composer phpstan. No errors are reported.
  2. Run composer phpstan:fix. It fails with "No fixable errors found", and changes nothing.
  3. In tools/phpstan/baseline.neon, raise the count of any entry.
  4. Run composer phpstan. The entry is reported as ignore.count, marked with 🔧.
  5. Run composer phpstan:fix. The count is lowered back, nothing else in the file changes, and the second analysis reports no errors.
  6. To check report.ts, write a report with composer phpstan -- --error-format=json > report.json while the count is raised, then run GITHUB_STEP_SUMMARY=summary.md npm run --workspace @wordpress/phpstan-tools report -- report.json. The annotation and summary.md mention composer phpstan:fix.

Use of AI Tools

Written with Claude Code using Claude Opus 5.5 (medium effort): researching the 2.3.5 changes, testing --fix against the baseline, and writing the version bump, the phpstan:fix script, documentation, and report.ts changes, all reviewed by me.

westonruter and others added 2 commits November 4, 2026 14:59
The analysis reports no errors with this release, and no baseline entry
goes stale, so the baseline is unchanged. The other PHPStan packages,
phpstan-phpunit (2.1.1), extension-installer (1.4.3), and
szepeviktor/phpstan-wordpress (2.0.4), are already at their latest
versions.

The `WP_REST_Request` offset ignores stay, since the PHPStan fix they
wait on (phpstan/phpstan-src#6903) is not yet merged.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tnky3GdCg3QL5KeMvuQvwf
Claude-Code-Session-Id: 5bac4eea-faaa-40e6-8440-b696b7773ba6
PHPStan 2.3.5 can fix stale ignores itself: `--fix` lowers a baseline
count that is now too high, removes an entry that matches nothing, and
removes an `@phpstan-ignore` comment that no longer ignores anything.
Verified on `tools/phpstan/baseline.neon`: an inflated count was lowered
back to its real value, leaving the rest of the file as it was.

That suits Gutenberg better than regenerating. `composer
phpstan:baselines` is PHPStan's `--generate-baseline`, which records
every error the run reports, so the job summary had to warn against
regenerating while other errors were still reported. `--fix` never adds
an entry, so it is safe to run then too.

The coding guidelines and the job summary now recommend `--fix`, and
explain that it has to be a run over all the paths, since PHPStan
reports no out-of-date entries when it analyzes only some files.

`report.ts` also reads the `fixable` flag that PHPStan's JSON report now
carries: an annotation of a fixable error says how to fix it, and the
job summary marks the error with 🔧, as PHPStan's table output does.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tnky3GdCg3QL5KeMvuQvwf
Claude-Code-Session-Id: 5bac4eea-faaa-40e6-8440-b696b7773ba6
@westonruter westonruter added the [Type] Build Tooling Issues or PRs related to build tooling label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground Preview in Storybook

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: westonruter <westonruter@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

📦 Bundle size

Size Change: 0 B

Total Size: 8.63 MB

ffbcddd Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric b2de714 trunk % Change
timeToFirstByte 56.2 ms +8.81% -3.38% 56.85 ms +4.57% -4.05% -1.14%
largestContentfulPaint 92 ms +6.52% -2.17% 92 ms +13.04% -4.35% 0%
lcpMinusTtfb 35.4 ms +12.43% -3.39% 34.75 ms +28.78% -4.75% 1.87%
wpBeforeTemplate 29.76 ms +6.28% -2.08% 29.7 ms +6.7% -3.2% 0.2%
wpTemplate 22.67 ms +3.09% -6.31% 22.73 ms +5.32% -4.71% -0.26%
wpTotal 52.21 ms +9.21% -3.24% 53.1 ms +4.69% -4.71% -1.68%
wpMemoryUsage 7.72 MB +0% -0% 7.69 MB +0% -0% 0.43%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric b2de714 trunk % Change
timeToFirstByte 48.65 ms +6.99% -3.91% 47.1 ms +3.82% -2.02% 3.29%
largestContentfulPaint 106 ms +1.89% -1.89% 100 ms +4% -0% 6%
lcpMinusTtfb 56.8 ms +1.94% -1.5% 54.35 ms +1.38% -1.56% 4.51%
wpBeforeTemplate 27.87 ms +6.32% -3.27% 26.94 ms +1.56% -1.41% 3.45%
wpTemplate 17.58 ms +3.53% -4.89% 16.8 ms +4.82% -3.57% 4.64%
wpTotal 45.45 ms +7.33% -4.05% 44.04 ms +3.59% -2.18% 3.2%
wpMemoryUsage 6.41 MB +0% -0% 6.38 MB +0% -0% 0.53%
wpDbQueries 13 +0% -0% 13 +0% -0% 0%

media-processing

Metric b2de714 trunk % Change
mediaProcessingJpeg 407.84 ms +0.48% -0.89% 403.05 ms +2.93% -0.29% 1.19%
mediaProcessingAvif 6181.2 ms +0.18% -0.4% 6184.12 ms +0.34% -0.32% -0.05%
mediaProcessingJpegToAvif 4284.84 ms +0.21% -0.76% 4275.5 ms +0.13% -0.08% 0.22%

media-upload

Metric b2de714 trunk % Change
jpegUploadProcessing 1952.29 ms +1.6% -23.27% 1464.42 ms +1.4% -0.74% 33.31%
pngUploadProcessing 283.81 ms +2.24% -33.6% 179.96 ms +4.43% -3.6% 57.71%
largeJpegUploadProcessing 1488.98 ms +31.01% -1.68% 1716.07 ms +13.27% -14.74% -13.23%
multipleImageUploadProcessing 2038.77 ms +0.6% -0.64% 2020.7 ms +2.95% -0.63% 0.89%

post-editor

Metric b2de714 trunk % Change
serverResponse 264.27 ms +6.85% -9.03% 297.62 ms +3.34% -7.34% -11.21%
firstPaint 261.34 ms +27.78% -3.03% 329.35 ms +16.81% -26.66% -20.65%
domContentLoaded 615.41 ms +8.91% -3.09% 661.18 ms +0.2% -0.43% -6.92%
loaded 616.3 ms +8.93% -3.09% 662.23 ms +0.22% -0.4% -6.94%
firstContentfulPaint 372.83 ms +3.69% -5.23% 387.62 ms +2.27% -1.17% -3.82%
firstBlock 1562.57 ms +6.3% -3.06% 1710.83 ms +0.4% -2.27% -8.67%
type 14.15 ms +7.49% -6.43% 16.12 ms +12.53% -8.56% -12.22%
typeWithoutInspector 13.85 ms +3.54% -9.89% 16.09 ms +8.64% -6.34% -13.92%
typeWithTopToolbar 15.66 ms +13.67% -4.41% 20.92 ms +11.76% -12.72% -25.14%
typeContainer 6.04 ms +8.28% -5.46% 7.8 ms +17.56% -5.38% -22.56%
focus 75.47 ms +4.21% -3.84% 89.46 ms +9.87% -5.87% -15.64%
firstFocus 151.04 ms +0% -0% 205.2 ms +0% -0% -26.39%
selectAll 279.45 ms +2.74% -9.73% 288.97 ms +2.92% -4.42% -3.29%
undo 19.26 ms +6.28% -7.42% 26.37 ms +3.72% -5.23% -26.96%
listViewOpen 48.9 ms +7.34% -7.79% 58.16 ms +23.3% -20.74% -15.92%
inserterOpen 26.85 ms +3.09% -2.53% 34.9 ms +16.99% -5.53% -23.07%
inserterHover 7.5 ms +9.2% -5.47% 7.62 ms +36.75% -8.27% -1.57%
inserterSearch 6.31 ms +5.86% -3.49% 8.19 ms +6.72% -6.23% -22.95%
loadPatterns 420.13 ms +3.2% -6.44% 405.74 ms +10.19% -2.68% 3.55%
wpTotal 257.22 ms +7.03% -9.14% 290.46 ms +3.28% -7.62% -11.44%
wpMemoryUsage 14.77 MB +0% -0% 14.74 MB +0% -0% 0.24%
wpDbQueries 55 +0% -0% 55 +0% -0% 0%

site-editor

Metric b2de714 trunk % Change
serverResponse 478.74 ms +2.84% -8.12% 478.09 ms +0.79% -8.46% 0.14%
firstPaint 356.37 ms +14.69% -7.76% 358.18 ms +5.59% -7.58% -0.51%
domContentLoaded 896.98 ms +2.43% -1.62% 876.83 ms +0.81% -1.11% 2.3%
loaded 898.5 ms +2.38% -1.66% 878.07 ms +0.82% -1.13% 2.33%
firstContentfulPaint 513.32 ms +4.32% -1.03% 500.17 ms +3.21% -0.53% 2.63%
firstBlock 2661.37 ms +1.83% -1.24% 2632.67 ms +0.47% -0.59% 1.09%
type 18.58 ms +7.37% -4.41% 19.44 ms +4.48% -2.78% -4.42%
navigate 120.87 ms +8.22% -0.99% 118.68 ms +3.52% -2.86% 1.85%
loadPatterns 1335.6 ms +2.4% -2.82% 1444.33 ms +5.08% -9.13% -7.53%
loadPages 1025.03 ms +0.65% -0.88% 1016.2 ms +1.42% -1.38% 0.87%
wpTotal 469.27 ms +2.87% -8.29% 468.55 ms +0.8% -8.62% 0.15%
wpMemoryUsage 12.79 MB +0% -0% 12.75 MB +0% -0% 0.27%
wpDbQueries 45 +0% -0% 45 +0% -0% 0%

ffbcddd Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

Page List converts to Navigation Links with entity bindings in /test/e2e/specs/editor/blocks/navigation.spec.js, passed after 1 failed attempt.
Error: expect(locator).toContainText(expected) failed

Locator: getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ })
Expected substring: "test-page-1"
Received string:    "Link to:Cat/catPagePublished"
Timeout: 5000ms

Call log:
  - Expect "toContainText" getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ }) with timeout 5000ms
  - waiting for getByRole('region', { name: 'Editor settings' }).getByRole('tabpanel', { name: 'Content' }).getByRole('button', { name: /Link to:/ })
    14 × locator resolved to <button type="button" aria-expanded="false" aria-haspopup="dialog" class="components-button link-preview-button is-next-40px-default-size is-secondary">…</button>
       - unexpected value "Link to:Cat/catPagePublished"

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/navigation.spec.js:1949:31
does not select blocks in the canvas preview when editing from the sidebar (Site Editor v2) in /test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js, passed after 1 failed attempt.
Error: expect(locator).toBeFocused() failed

Locator: getByRole('combobox', { name: 'Search or type URL' })
Expected: focused
Timeout: 5000ms
Error: element(s) not found

Call log:
  - Expect "toBeFocused" getByRole('combobox', { name: 'Search or type URL' }) with timeout 5000ms
  - waiting for getByRole('combobox', { name: 'Search or type URL' })

    at LinkControl.useLinkControlSearch (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js:416:47)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js:179:21
should place the caret at the click position when clicking a block that is not selected in /test/e2e/specs/editor/various/writing-flow.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 1
+ Received  + 1

@@ -1,9 +1,9 @@
  Array [
    Object {
      "attributes": Object {
-       "content": "First!",
+       "content": "First",
      },
      "name": "core/paragraph",
    },
    Object {
      "attributes": Object {

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/writing-flow.spec.js:952:41

ffbcddd Run

@westonruter
westonruter marked this pull request as ready for review November 4, 2026 22:05
Resolves the conflict in the coding guidelines, where trunk split the
PHPStan paragraph into subsections. Trunk's structure is kept, and the
recommendation of `--fix` for out-of-date baseline entries is applied
to its "The baseline" subsection.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tnky3GdCg3QL5KeMvuQvwf
Claude-Code-Session-Id: 5bac4eea-faaa-40e6-8440-b696b7773ba6
@westonruter
westonruter requested a review from ramonjd November 10, 2026 02:11
@westonruter
westonruter marked this pull request as draft November 10, 2026 02:12
westonruter and others added 2 commits November 9, 2026 18:51
Removing an out-of-date baseline entry meant running
`composer phpstan -- --fix`. Leaving out the `--` makes Composer discard
the flag silently, so the analysis just reports the stale entries again.

`composer phpstan:fix` (and `npm run typecheck:php:fix`) chains two
steps, the second run only when the first succeeds:

* PHPStan with `--fix`, which lowers or removes the stale entries.
* The analysis once more, since PHPStan's `--fix` reports only what it
  fixed, and succeeds even when other errors remain. It reuses the
  result cache, so it is quick.

Both analyses use `phpstan.neon.dist`, whatever a local `phpstan.neon`
says. A configuration at another rule level can report a baselined error
with a different message, and `--fix` then removes entries the default
configuration still needs.

When nothing is fixable, PHPStan exits with 1 and stops the chain, which
the coding guidelines note.

The coding guidelines, the README, and the CI annotation and job summary
now point to the new scripts.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tnky3GdCg3QL5KeMvuQvwf
Claude-Code-Session-Id: 5bac4eea-faaa-40e6-8440-b696b7773ba6
The comment above the baseline include in `phpstan.neon.dist` still
recommended `composer phpstan -- --fix`.

Co-Authored-By: Claude Opus 5.5 (medium effort) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tnky3GdCg3QL5KeMvuQvwf
Claude-Code-Session-Id: 5bac4eea-faaa-40e6-8440-b696b7773ba6
@westonruter
westonruter marked this pull request as ready for review November 10, 2026 04:41
@westonruter

Copy link
Copy Markdown
Member Author

The companion core PR has been merged: WordPress/wordpress-develop#15479.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are identified across the PHPStan tooling updates, CI guidance, configuration, and contributor documentation.

No open issues 🎉

What changed in this PR

Updates Gutenberg’s PHPStan tooling to version 2.3.5 and introduces a dedicated workflow for removing stale baseline entries without regenerating the entire baseline.

Changes:

  • Adds composer phpstan:fix and npm run typecheck:php:fix with a follow-up analysis for remaining errors.
  • Updates CI reporting to identify fixable PHPStan errors and recommend the new workflow.
  • Revises PHPStan configuration and contributor documentation.
File Description
tools/​phpstan/​report.ts Reports PHPStan errors marked as fixable and includes guidance for running the baseline-fix command.
tools/​phpstan/​README.md Documents stale baseline cleanup using the new PHPStan fix workflow.
phpstan.neon.dist Updates PHPStan baseline maintenance guidance.
package.json Adds the local-environment wrapper for the PHPStan baseline-fix command.
docs/​contributors/​code/​coding-guidelines.md Replaces baseline regeneration guidance with instructions for the safer fix workflow.
composer.json Updates PHPStan to 2.3.5 and adds the two-step baseline-fix Composer script.

Review effort: Lite

@ramonjd ramonjd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a great authority on this sort of stuff so take my review with all the salt you need.

Thanks for the upgrade. I ran the commands and they behaved as expected.

Using --fix to clean the baseline makes sense from what I can understand.

I got Claude to look into PHPStan 2.3.5's source to check the exit codes the PR relies on, and they match.

One thing I noticed in the terminal - composer phpstan:fix fails when there’s nothing to fix. would that be confusing for some folks? i don't know about best practice here. could the script should treat "nothing to fix" as something that's desirable, like success?

Screenshot 2026-11-11 at 11 22 40 am

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Build Tooling Issues or PRs related to build tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants