Skip to content

Update PHPStan to v2, improve code quality#1729

Merged
westonruter merged 33 commits into
trunkfrom
update/phpstan
Jun 22, 2026
Merged

Update PHPStan to v2, improve code quality#1729
westonruter merged 33 commits into
trunkfrom
update/phpstan

Conversation

@swissspidy

@swissspidy swissspidy commented Dec 10, 2024

Copy link
Copy Markdown
Member

Summary

Fixes #776

Relevant technical choices

  • Updates PHPStan and extensions to v2
  • Moves PHPStan dependencies to a new phpstan folder, because PHPStan v2 requires PHP 7.4, whereas this repo requires PHP 7.2+
  • Requires swissspidy/phpstan-no-private
  • Moves hooks in the PL plugin to hooks.php like in the other plugins for consistency, makes it easier to separate that code from the constant definitions in load.php

@swissspidy swissspidy added the [Type] Enhancement A suggestion for improvement of an existing feature label Dec 10, 2024
@swissspidy swissspidy added this to the performance-lab n.e.x.t milestone Dec 10, 2024
@swissspidy

Copy link
Copy Markdown
Member Author

Strangely I cannot reproduce the https://github.com/WordPress/performance/actions/runs/12256447140/job/34191719008?pr=1729 error locally right now 🤔 I got it earlier but thought I had resolved it.

Comment thread composer.json Outdated
"phpstan": "phpstan analyse --memory-limit=2048M",
"phpstan": [
"composer --working-dir=phpstan install --no-interaction",
"phpstan/vendor/bin/phpstan analyse --memory-limit=2048M"

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.

So there is a new phpstan root directory?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes. Could also be renamed.

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.

We have an existing tools/phpstan directory that could be reused.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm not sure I wanna mix the config directory with that one

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Since we had build-cs before I could just rename it to that.

@westonruter

Copy link
Copy Markdown
Member
  • Moves PHPStan dependencies to a new phpstan folder, because PHPStan v2 requires PHP 7.4, whereas this repo requires PHP 7.2+

This is unfortunate since we previously maintained two separate composer.json files but were able to go to one, as I recall.

I wonder if we should bump the minimum PHP version to 7.4 for the entire project to facilitate maintenance and considering that core will likely bump the minimum version to 7.4 in in the middle of 2025 per Core-62622. We could still run PHPUnit on 7.2 in GHA for now if we want to.

@swissspidy

Copy link
Copy Markdown
Member Author

Ah true, there was a build-cs folder before, but I can't see where it was removed at first glance.

Bumping requirements is of course the easiest route, but I thought we just recently discussed not bumping PHP version requirements unnecessarily if we can avoid it.

@swissspidy

Copy link
Copy Markdown
Member Author

Strangely I cannot reproduce the WordPress/performance/actions/runs/12256447140/job/34191719008?pr=1729 error locally right now 🤔 I got it earlier but thought I had resolved it.

@westonruter is it passing for you locally?

@westonruter

Copy link
Copy Markdown
Member

Ah true, there was a build-cs folder before, but I can't see where it was removed at first glance.

It was removed when we bumped to PHP 7.2 in #1130.

@westonruter westonruter removed this from the performance-lab n.e.x.t milestone Feb 7, 2025
@westonruter westonruter added no milestone PRs that do not have a defined milestone for release Infrastructure Issues for the overall performance plugin infrastructure labels Jun 20, 2026
westonruter and others added 2 commits June 19, 2026 19:23
Resolve the 16-month-stale conflicts between PR #1729 (PHPStan v2) and trunk:

- load.php / hooks.php: keep the PR's move of hooks into hooks.php, and
  port trunk's in-place changes onto it (standalone plugin list now includes
  nocache-bfcache and view-transitions, drops web-worker-offloading as
  experimental; PERFLAB_PLACE_OBJECT_CACHE_DROPIN gating; PHP 7.4 / 4.1.0
  header bumps).
- composer.json / composer.lock: collapse the PR's separate phpstan/
  subfolder back into the root. The subfolder existed only because PHPStan v2
  requires PHP 7.4 while the plugin required 7.2; trunk now requires 7.4, so
  the PHPStan v2 dev-dependencies live directly in the root composer.json.
  PHPStan is pinned to 2.2.2 with the v2 extension set, plus
  swissspidy/phpstan-no-private. The phpstan/ subfolder is removed.
- phpstan.neon.dist: drop the phpVersion override (inferred from
  config.platform.php = 7.4), restore root vendor/ paths for php-8-stubs,
  keep the no.private test ignores, and drop the empty.notAllowed block that
  trunk already removed.

Static-analysis errors surfaced by the v2 upgrade are addressed separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile the ignore configuration with PHPStan v2's stricter rules and
renamed identifiers, without suppressing genuine type errors (those are
left as follow-up):

- Add per-plugin no.private ignores for intra-plugin private API usage
  surfaced by swissspidy/phpstan-no-private across embed-optimizer,
  image-prioritizer, speculation-rules, view-transitions and
  performance-lab, matching the existing dominant-color-images /
  optimization-detective / webp-uploads / web-worker-offloading entries.
- Ignore test-only strictness (phpunit.assertEquals, catch.internalClass)
  consistent with the existing "excessively strict for tests" ignores.
- Update the file-not-found ignore to v2's require.fileNotFound identifier
  and drop the now-unmatched includeOnce.fileNotFound and test-scoped
  offsetAccess.invalidOffset / no.private.class entries.
- Remove three stale inline @PHPStan-Ignore comments that v2 no longer
  needs (offsetUnset is now understood to throw; impure return value is
  remembered correctly).

The remaining genuine type errors (argument.type, return.type, etc.) are
intentionally left unaddressed in this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@westonruter

Copy link
Copy Markdown
Member

Finally picking this back up...

@github-actions github-actions Bot added [Plugin] Modern Image Formats Issues for the Modern Image Formats plugin (formerly WebP Uploads) [Plugin] Performance Lab Issue relates to work in the Performance Lab Plugin only [Plugin] Optimization Detective Issues for the Optimization Detective plugin labels Jun 20, 2026
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.55556% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.03%. Comparing base (ea77bb1) to head (1c6711c).

Files with missing lines Patch % Lines
plugins/performance-lab/hooks.php 71.42% 26 Missing ⚠️
plugins/performance-lab/includes/admin/plugins.php 0.00% 2 Missing ⚠️
plugins/optimization-detective/uninstall.php 50.00% 1 Missing ⚠️
plugins/performance-lab/load.php 0.00% 1 Missing ⚠️
plugins/view-transitions/includes/settings.php 0.00% 1 Missing ⚠️
plugins/view-transitions/uninstall.php 0.00% 1 Missing ⚠️
plugins/webp-uploads/uninstall.php 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk    #1729      +/-   ##
==========================================
+ Coverage   69.87%   70.03%   +0.16%     
==========================================
  Files          90       91       +1     
  Lines        7797     7803       +6     
==========================================
+ Hits         5448     5465      +17     
+ Misses       2349     2338      -11     
Flag Coverage Δ
multisite 70.03% <75.55%> (+0.16%) ⬆️
single 35.42% <57.03%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The multisite uninstall routines call `get_sites( array( 'fields' => 'ids', ... ) )`,
which returns an array of integer site IDs at runtime. However,
php-stubs/wordpress-stubs types the conditional return of `get_sites()` with a
*sealed* array shape (`$args is array{fields: 'ids'}`), so the `int[]` narrowing
only applies when `fields => 'ids'` is the sole key. Passing additional query
args (`number`, `update_site_cache`, …) makes the argument no longer a subtype
of the sealed shape, and the return type falls back to `array<int, WP_Site>` —
producing false `argument.type` errors on `switch_to_blog()` and `array_diff()`.

Suppress these with inline `@phpstan-ignore argument.type` directives carrying a
TODO. An `@phpstan-ignore` (rather than an inline `@var`) is used deliberately:
with `reportUnmatchedIgnoredErrors` enabled, the suppression becomes an
unmatched-ignore error once the upstream stub is corrected to use an unsealed
shape (`array{fields: 'ids', ...}`), automatically flagging the workaround for
removal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the [Plugin] Speculative Loading Issues for the Speculative Loading plugin (formerly Speculation Rules) label Jun 20, 2026
@github-actions

Copy link
Copy Markdown

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 props-bot label.

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: swissspidy <swissspidy@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>

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

@westonruter

Copy link
Copy Markdown
Member

Note: This comment was written by Claude (Claude Code), pair-programming with @westonruter. It summarizes the work done to bring this stale PR up to date and green.

Summary of changes

This PR was ~16 months stale and conflicting with trunk. Here's everything that was done to get it merge-ready and passing.

1. Merge trunk and resolve conflicts

Merged current trunk into the branch and resolved the 7 conflicting files:

  • load.php / hooks.php — kept this PR's move of Performance Lab hooks into hooks.php, and ported trunk's in-place changes onto it (standalone-plugin list now includes nocache-bfcache/view-transitions and drops web-worker-offloading as experimental; PERFLAB_PLACE_OBJECT_CACHE_DROPIN gating; PHP 7.4 / 4.1.0 header bumps).
  • composer.json / composer.lock — see below.
  • phpstan.neon.dist — reconciled both sides.

2. PHPStan upgraded to v2.2.2, collapsed into the root

The original PR isolated PHPStan in a separate phpstan/ sub-composer.json because PHPStan v2 needs PHP 7.4 while the plugin still supported 7.2. Since trunk now requires PHP 7.4, that rationale is gone, so the PHPStan v2 dev-dependencies were moved back into the root composer.json (phpstan/phpstan pinned to 2.2.2, plus the v2 extension set and swissspidy/phpstan-no-private), the phpstan/ subfolder was removed, and the lock file regenerated.

3. Drove PHPStan from 181 errors → 0

After the merge + v2 upgrade, PHPStan reported 181 errors (16 months of new code only ever checked under v1, now analyzed by the stricter v2 + extensions). These were resolved across focused commits:

group count how
Config / test strictness / no.private ~75 per-plugin no.private ignores, test-only identifier ignores, v2 identifier renames, removal of stale/unmatched ignores
return.type 22 corrected @return array-shapes — heavy use of unsealed array{…, ...} shapes for schema/filter/site-health/HTTP/webp-metadata functions
argument.type 15 webp metadata-flow param shapes (canonical sizes shape with sources?); over-strict-WP-core-stub suppressions (see below)
offsetAccess.*, assign.byRefForeachExpr, sprintf casts, singletons rest null guards, unset() after by-ref foreach, value casts, etc.

Two latent bugs were uncovered and fixed in the process:

  • webp_uploads_generate_additional_image_source() — the "supply the filesize from the filter-provided path" fallback checked is_int( $image['path'] ), but path is a string, so the branch was dead code and never ran. Now uses array_key_exists().
  • A redundant is_string() / dead branch cleanup in the same area.

4. Over-strict WordPress core stubs

Three errors are not bugs in our code — they're cases where php-stubs/wordpress-stubs is stricter than WordPress's documented contract:

  • get_sites() — its conditional return type uses a sealed array shape, so the fields => 'ids'int[] narrowing is lost when extra query args are passed.
  • plugins_api()$args shape omits the fields sub-array.
  • add_settings_field()$args is documented as arbitrary, but stubbed as a sealed array{label_for?, class?}.

These are suppressed with inline @phpstan-ignore directives carrying a TODO to fix the stubs upstream. With reportUnmatchedIgnoredErrors enabled, they self-flag for removal once the upstream stubs are loosened.

5. Validation

  • ✅ PHPStan v2.2.2: 0 errors
  • ✅ Full PHPUnit plugin suite: passing (~1,270 tests, ~14,500 assertions; only environment-gated skips)
  • ✅ Pre-commit hooks (PHPCS + PHPStan) pass

Follow-up

Worth filing upstream PRs against php-stubs/wordpress-stubs for the three stubs above; the inline ignores will then surface as unmatched and can be removed.

The PHPCS doc-comment sniff cannot parse the unsealed array-shape marker (...)
inside a plain @param tag — it fails to find the parameter name, producing
"Missing parameter name" / "Doc comment for parameter missing" errors.

Move the unsealed array shapes to @phpstan-param tags (which PHPCS ignores and
PHPStan reads), keeping a simpler PHPCS-friendly @param for documentation, in
the functions where unsealed @param shapes were introduced:
webp_uploads_should_discard_additional_image_file(),
image_prioritizer_add_root_schema_properties(),
embed_optimizer_add_element_item_schema_properties(),
od_add_rest_api_availability_test(), perflab_aea_add_enqueued_assets_test().

Also realign the perflab_get_plugin_availability() @param block after the
experimental?: bool addition lengthened the type column (phpcbf).

PHPStan remains at zero errors and PHPCS passes on all tracked files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 20, 2026 20:16

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.

Pull request overview

This PR updates the repo’s static analysis tooling to PHPStan v2 (plus related extensions, including swissspidy/phpstan-no-private) and makes a set of targeted code/docblock adjustments across plugins so the codebase type-checks cleanly under the stricter analyzer while preserving runtime behavior.

Changes:

  • Upgrades PHPStan + extensions (incl. phpstan-wordpress v2, phpstan-phpunit v2, phpstan-strict-rules v2) and adds swissspidy/phpstan-no-private.
  • Refines PHPStan configuration/stubs and adjusts various docblocks/array-shapes and a few code patterns to satisfy PHPStan v2.
  • Refactors Performance Lab’s hook callbacks out of load.php into a dedicated hooks.php.

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tools/phpstan/filtered-functions.stub Loosens array-shapes to allow additional keys for improved stub compatibility.
tools/phpstan/constants.php Adjusts PHPStan bootstrap constants used to model plugin runtime constants.
plugins/webp-uploads/uninstall.php Adds targeted PHPStan ignore for switch_to_blog() arg typing.
plugins/webp-uploads/tests/test-image-edit.php Removes a PHPStan ignore workaround no longer needed under v2 behavior.
plugins/webp-uploads/rest-api.php Unsets by-ref loop variables to avoid accidental reference leakage.
plugins/webp-uploads/image-edit.php Updates PHPStan array-shapes to allow extra keys / new sources structure.
plugins/webp-uploads/hooks.php Adds PHPStan ignore for pseudo-private WP core function usage + null guard logic.
plugins/webp-uploads/helper.php Tightens return shape for filtered image data; adjusts docblocks.
plugins/web-worker-offloading/helper.php Casts wp_json_encode() result to satisfy stricter typing.
plugins/view-transitions/uninstall.php Adds targeted PHPStan ignore for switch_to_blog() arg typing.
plugins/view-transitions/includes/theme.php Casts wp_json_encode() result to satisfy stricter typing.
plugins/view-transitions/includes/settings.php Adds targeted PHPStan ignore for add_settings_field() args typing.
plugins/speculation-rules/uninstall.php Adds targeted PHPStan ignore for switch_to_blog() arg typing.
plugins/speculation-rules/settings.php Preserves sealed array-shape by returning explicit array literal; adds targeted ignore.
plugins/performance-lab/load.php Loads new hooks.php and removes hook callback definitions from load.php.
plugins/performance-lab/includes/site-health/audit-enqueued-assets/hooks.php Refines docblocks/return shapes for PHPStan v2.
plugins/performance-lab/includes/site-health/audit-enqueued-assets/helper.php Refines array-shapes (allow extra keys) and corrects return shape docblock.
plugins/performance-lab/includes/site-health/audit-autoloaded-options/helper.php Casts size_format() result to string for stricter typing.
plugins/performance-lab/includes/admin/plugins.php Adds targeted PHPStan ignore for plugins_api() args typing; refines docblocks.
plugins/performance-lab/hooks.php New file: extracted Performance Lab hook callbacks from load.php.
plugins/optimization-detective/uninstall.php Adds targeted PHPStan ignores for array_diff() + switch_to_blog() typing.
plugins/optimization-detective/tests/test-class-od-element.php Removes PHPStan ignores around exception flow.
plugins/optimization-detective/site-health.php Refines docblocks/array-shapes for PHPStan v2.
plugins/optimization-detective/optimization.php Casts wp_json_encode() result to satisfy stricter typing.
plugins/optimization-detective/helper.php Replaces wp_array_slice_assoc() usage with a PHPStan-friendlier key intersection.
plugins/optimization-detective/class-od-url-metric.php Allows extra keys in schema-related array-shape docblocks.
plugins/optimization-detective/class-od-url-metric-group.php Avoids offset access on possibly-null key result to satisfy PHPStan.
plugins/optimization-detective/class-od-html-tag-processor.php Adds targeted ignore for pseudo-private WP class usage.
plugins/image-prioritizer/helper.php Refines docblocks/array-shapes for PHPStan v2.
plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php Tightens docblock return semantics for a method that always returns false.
plugins/embed-optimizer/tests/test-hooks.php Updates data provider return shape for added optional value.
plugins/embed-optimizer/helper.php Refines docblocks/array-shapes for PHPStan v2.
plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php Adjusts types/casts and CSS escaping edge-case for stricter analysis.
plugins/dominant-color-images/hooks.php Refines docblocks/array-shapes to allow extra keys and align with PHPStan v2.
phpstan.neon.dist Updates ignore rules and stubs for PHPStan v2 and phpstan-no-private integration.
composer.lock Locks updated PHPStan ecosystem versions and adds phpstan-no-private.
composer.json Updates dev dependencies to PHPStan v2-compatible versions and adds phpstan-no-private.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugins/webp-uploads/helper.php Outdated
Comment thread plugins/performance-lab/hooks.php
Comment thread plugins/performance-lab/hooks.php
Comment thread tools/phpstan/constants.php Outdated
Comment thread tools/phpstan/constants.php Outdated
Comment thread tools/phpstan/constants.php Outdated
Comment thread phpstan.neon.dist Outdated
@westonruter

Copy link
Copy Markdown
Member

We're in the green!!

I'll admit I was surprised when I realized this plugin was still on v1.

@swissspidy Thank you for getting this started so long ago.

westonruter and others added 5 commits June 20, 2026 14:24
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Following up on Copilot's review feedback that the require/requireOnce
fileNotFound ignores were unscoped (and would hide real missing-require
regressions): give TESTS_PLUGIN_DIR a distinctive, non-resolvable sentinel
value and scope those ignores to it via a message regex.

TESTS_PLUGIN_DIR is set to the plugin-under-test directory at runtime, so it
varies per suite and cannot be modeled statically; its require() targets
(uninstall.php and conditionally-loaded files) are unavoidable false positives.
TESTS_REPO_ROOT_DIR is left as './' because it correctly resolves to the repo
root (PHPStan's working directory), so its requires resolve and those files are
actually analyzed.

This keeps real require regressions reported everywhere except the known
TESTS_PLUGIN_DIR false-positive pattern.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Following up on Copilot's review: webp_uploads_generate_additional_image_source()
calls wp_filesize( $image['path'] ), but $image comes from the pluggable
webp_uploads_pre_generate_additional_image_source filter, so a third party could
return a non-string `path`, causing a TypeError (wp_filesize() requires a string).

Restore the is_string( $image['path'] ) runtime guard, and type the filter's
`path` as non-empty-string|mixed so the untrusted value makes the check
meaningful to PHPStan (rather than being flagged as alreadyNarrowedType) while
still documenting that it is normally a non-empty path.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0x002D === $code_unit
) {
$result .= '\\' . $ident[ $i ];
$result .= '\\-';

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.

Note from Claude about this change:


Now I can explain all three parts precisely.

What the error means

Offset *NEVER* does not exist on non-empty-string.

$ident[$i] is a string-offset access. The *NEVER* is the type PHPStan inferred for the offset expression $i at that line — never (its "bottom"/impossible type). So PHPStan is really saying: "by the time control reaches line 259, $i has type never, i.e. I believe this line is unreachable — and you can't index a string with an impossible offset." When the offset is never, it phrases it as "offset NEVER does not exist."

(The "on non-empty-string" part is just because, inside the for loop, the loop can only run when strlen($ident) >= 1, so PHPStan has narrowed $ident from string to non-empty-string.)

Why it's never at 259 but fine at 280

The difference is the guard around 259:

if ( 0 === $i && 1 === $length && 0x002D === $code_unit ) {
    $result .= '\\' . $ident[ $i ];   // 259

Those equality constraints (0 === $i and 1 === $length) feed PHPStan's loop/offset analysis, and under bleedingEdge it wrongly concludes this exact combination can't occur — collapsing $i to never. It's a false positive: escape_css("-") (a single hyphen) genuinely reaches line 259 at runtime — trace it: $length=1, $i=0, $code_unit=ord('-')=0x002D, it skips the earlier ifs, and hits this branch.

Line 280 has no such narrowing guard — it's the loop's plain "otherwise" fall-through, where $i is just the normal int<0, max> loop index, which is a valid offset on the string. So no error there. Same text, different inferred type for $i.

Why I replaced it with '\\-'

The branch is guarded by 0x002D === $code_unit, and $code_unit = ord( $ident[$i] ) — so inside this branch $ident[$i] is provably the character - (chr(0x2D)). Therefore:

'\\' . $ident[ $i ]   // is always exactly  '\-'
'\\-'                 // identical output

Replacing it with the literal:

  • produces byte-for-byte identical output, and
  • removes the $ident[$i] access that PHPStan was (incorrectly) flagging — without an @phpstan-ignore suppression.

Line 280 keeps $ident[$i] because there the character is arbitrary (could be anything), so the literal substitution wouldn't apply.

I'll be honest: I can't fully reverse-engineer PHPStan's exact internal step that produces the never here — it's a known-ish quirk of its string-offset reasoning inside loops with equality-narrowed indices. The substitution is a clean, behavior-preserving way around it.

If you'd prefer to keep the code symmetric with line 280 instead, the alternative is to restore '\\' . $ident[ $i ] and add an // @phpstan-ignore offsetAccess.notFound (false positive: $i is 0 for a single "-") — but I lean toward the literal since it's provably equivalent and arguably clearer. Want me to switch to the ignore-based version?

westonruter and others added 2 commits June 20, 2026 19:03
… map

The most-common-LCP-element computation used three arrays kept in sync by hand
($seen_breadcrumbs, $breadcrumb_counts, $breadcrumb_element). PHPStan cannot
correlate keys across separate arrays, so taking key() from one and indexing
another produced offsetAccess.invalidOffset / offsetAccess.notFound and needed
a defensive isset()/null guard.

Combine them into a single $breadcrumbs map keyed by element XPath holding
{count, element}. This removes the out-of-sync risk, replaces the O(n)
array_search() lookup with an O(1) keyed isset(), and lets reset() on the
count-guarded non-empty array return the record directly, so no isset()/null
dance is needed. Behavior is unchanged: last-seen element per breadcrumb, most
frequent wins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread plugins/webp-uploads/helper.php
…te_additional_image_source()

Covers the branch where the webp_uploads_pre_generate_additional_image_source
filter short-circuits with a file and path but no filesize, in which case the
filesize is derived via wp_filesize() from the filter-provided path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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.

Pull request overview

Copilot reviewed 37 out of 38 changed files in this pull request and generated 1 comment.

Comment thread plugins/performance-lab/hooks.php Outdated
westonruter and others added 4 commits June 21, 2026 18:38
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…_editor_output_format()

Covers the two leading conditionals of the previously-untested filter: a
non-array output format is normalized to an empty array, and an existing
mapping is returned unchanged when the source mime type is null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avoid setting 'element' twice on a breadcrumb's first occurrence: initialize it
with count 1 (and the element) when first seen, and only increment the count and
override the element on subsequent occurrences. Since the count now starts at 1
and only ever increments, type it as int<1, max>. Behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@westonruter westonruter merged commit 5fc2727 into trunk Jun 22, 2026
31 checks passed
@westonruter westonruter deleted the update/phpstan branch June 22, 2026 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Infrastructure Issues for the overall performance plugin infrastructure no milestone PRs that do not have a defined milestone for release [Plugin] Embed Optimizer Issues for the Embed Optimizer plugin (formerly Auto Sizes) [Plugin] Image Placeholders Issues for the Image Placeholders plugin (formerly Dominant Color Images) [Plugin] Image Prioritizer Issues for the Image Prioritizer plugin (dependent on Optimization Detective) [Plugin] Modern Image Formats Issues for the Modern Image Formats plugin (formerly WebP Uploads) [Plugin] Optimization Detective Issues for the Optimization Detective plugin [Plugin] Performance Lab Issue relates to work in the Performance Lab Plugin only [Plugin] Speculative Loading Issues for the Speculative Loading plugin (formerly Speculation Rules) [Plugin] View Transitions Issues for the View Transitions plugin [Plugin] Web Worker Offloading Issues for the Web Worker Offloading plugin. [Type] Enhancement A suggestion for improvement of an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add phpstan-no-private to catch calls of seemingly private functions

3 participants