Skip to content

Fix static analysis regressions introduced during the 7.1 cycle - #13064

Closed
westonruter wants to merge 17 commits into
WordPress:trunkfrom
westonruter:fix/phpstan-7.1-regressions
Closed

Fix static analysis regressions introduced during the 7.1 cycle#13064
westonruter wants to merge 17 commits into
WordPress:trunkfrom
westonruter:fix/phpstan-7.1-regressions

Conversation

@westonruter

@westonruter westonruter commented Aug 16, 2026

Copy link
Copy Markdown
Member

✅ Committed in:


Fixes the static analysis regressions introduced during the 7.1 cycle: errors that trunk reports and a baseline generated from 7.0.0's src does not.

Each was verified before being touched. An error is only treated as a regression if the symbol whose type provoked it was itself changed after the 7.0 tag — following the symbol rather than the reporting line, because a docblock edit in one file surfaces errors in files that have not been edited in years. r62178 changing WP_Widget::form() produced 20 errors in widget subclasses, every one of them on untouched code.

Deliberately out of scope: errors that appeared because an annotation became more accurate. The largest group follows r62529 giving wpdb::get_col() and friends precise return types, which made pre-existing call-site looseness checkable for the first time. That code is as old as 2012 and unchanged; it is technical debt, not a regression. Same for the @return never annotations that made long-standing defensive code visibly unreachable.

Of the 63 distinct symbols and sites behind the remaining errors, 9 had changed since 7.0.0. Every genuine regression traced back to a type-annotation or code-quality commit rather than to feature work.

Rebased on trunk

The branch sat for a while and has now been merged with trunk (69 revisions). Three files conflicted:

  • src/wp-includes/class-wp-block-type.php — trunk removed a stray blank line between @param and @return in __get()'s docblock while this branch widened the @return type. Both changes kept.
  • tests/phpstan/baselines/argument.type.neon and tests/phpstan/baselines/return.missing.neon — taken from trunk, after which every baseline was regenerated with composer phpstan:baselines against the merged tree rather than hand-merged. These files are generated artifacts; resolving them by hand invites entries that no longer correspond to a reported error.

Two pieces of this work landed in trunk in the meantime and are no longer under review here:

  • r63296 ("Formatting: Document that escaping functions can take numbers", abc50fe) came out of the same review. It widened the esc_*() annotations to string|int|float and cleared 54 baseline entries. Worth noting it was not a regression fix: 87 of the 103 errors it resolved already existed at 7.0.0.
  • r63379 ("Code Quality: Resolve the remaining return.missing PHPStan errors", developed in Remove return.missing baseline and fix its issue #13082) restored string|void on WP_Widget::form(), which is what commit d875967 below does. That commit is retained in this branch's history but is now a no-op — the file is byte-identical to trunk — and tests/phpstan/baselines/return.missing.neon is gone along with it.

Committing to SVN

These commits may be landed in SVN separately rather than as one changeset. Each is self-contained: it changes one thing, regenerates the affected baseline, and leaves the tree green on its own. The table gives the revision(s) each would be a follow-up to.

Git commit Follow-up to What it fixes
77dc415 Allow an integer for WP_Comment's two ID properties r62822 numeric-string is contradicted by get_comment_to_edit(), which casts both to int in place. Clears 39 baseline entries across 17 files.
84c63c3 Correct term_exists()'s conditional return for the no-taxonomy case r62680 The conditional type says int|null for the empty-$taxonomy branch; that branch does return (string) $_term.
5df0317 Include the array returns in WP_Block_Type::__get()'s type r62178 Every path returns a value, so the union needed completing with the array[] from get_variations().
cbcdd48 Document comment_shortcuts and infinite_scrolling on WP_User r9217, r62632, r63020 Committed as r63383. A new user preference read through __get() without being listed among the class's @property tags, unlike rich_editing beside it.
b24d67b Stop baselining a substr_compare() report PHPStan gets wrong r62667 Not a code defect. PHPStan's pre-8.0 functionMap.php drops the implicit nullability of $length; moved to ignoreErrors with the reasoning recorded.
cf9a4af Declare the $wpdb global on the Users screen r62688 New queries on a screen that never imported the global.
60664f9 Drop isset() checks on properties that are always set r62453, r62838 isset() paired with ! empty() / is_array() on declared properties with non-null defaults.
b7a00c1 Drop a redundant empty check in merge_properties() r62834 array() !== $current after ! array_is_list( $current ), which already implies non-empty.
5c6a81b Document get_feature_declarations_for_node()'s params as arrays r62444, r62607 Annotated object since 6.3.0 but only ever passed arrays. The by-reference $node propagated the wrong type back to callers. 5 errors from one docblock.
97f1544 Call WP_Theme_JSON's private static methods through self r52744, r63020, r63368 Committed as r63384, extended there from the 7 private statics that changed after 7.0.0 to all 14, which empties staticClassAccess.privateMethod.neon and deletes it. static:: resolves to the runtime class, where a private method is not visible.
d875967 Restore string|void on WP_Widget::form() r62178 Already landed as r63379 — no diff remains. void removed from a union tightened the contract; 18 subclass form() overrides echo and return nothing. 20 errors.

Against trunk, the branch now clears 58 baseline entries / 99 errors and adds 20 lines to phpstan.neon.dist.

Not fixed, and why

  • _upgrade_cron_array() (r62488) — the @phpstan-return shape is correct about runtime behaviour, but building an array key-by-key and then setting version collapses PHPStan's inference to non-empty-array<'version'|int, …>, losing the key/value correlation. Weakening a correct type to satisfy the analyzer seemed worse than leaving it baselined.
  • WP_Post property reports (r62717) — $_wp_attachment_image_alt is read through __get(), which accepts any meta key, so unlike the WP_User case above it cannot be resolved with a @property tag.

Verification

Every fix was checked with a full analysis before regenerating baselines, so that "no new errors" means the change introduced none rather than that regeneration absorbed them. Relevant suites were run per change: 316 theme.json, 794 comment, 104 block type, 78 view config, 46 block supports states, 26 term_exists.

After merging trunk, vendor/bin/phpstan analyse --configuration=phpstan.neon.dist — the configuration CI runs — reports no errors on the merged tree. Checking the branch in isolation with phpstan-diff --changed --base=origin/trunk is also clean. It previously left 15 reports, all pre-existing mixed-argument errors in class-wp-theme-json.php sitting on lines 97f1544 touched; those lines are upstream as of r63384, so they are no longer part of this branch's diff.

Trac ticket: Core-65817

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Building the 7.0.0-vs-trunk comparison, classifying each error by whether the symbol behind it changed after the tag, the fixes themselves, resolving the trunk merge, and drafting this description. Several of its intermediate classifications were wrong and were corrected after I pushed back on them; the scope decisions, and the final read of each fix, are mine.


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.

westonruter and others added 11 commits August 14, 2026 11:24
r62178 replaced `@return string|void` with `@return string|null` on
WP_Widget::form(), on the premise that void cannot be part of a union type.
That holds for PHP's native return types, but not for PHPDoc, where
`string|void` is the documented way to say a method may return a string or
may return nothing at all. PHPStan reads it exactly that way, and treats
`string|null` instead as an obligation to return.

The base implementation echoes a notice and returns 'noform', while every
subclass echoes its own markup and falls off the end. Tightening the declared
type therefore put all 18 subclass form() overrides in breach of it, for 20
reported errors. Four of those overrides live in bundled themes, which r62178
did not touch and so could not have updated alongside the parent.

form_callback() is unaffected: it initialises $return to null and always
returns it, so its own string|null annotation stays accurate, as does the
null|string documented for $return on the in_widget_form action. Only the
method that may legitimately not return at all needed void back.

Regenerating the baselines drops 18 entries covering 20 errors from
tests/phpstan/baselines/return.missing.neon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sixteen call sites reached seven private static methods through `static::`
rather than `self::`. Late static binding resolves to the runtime class, but
a private method is not inherited, so the two are only equivalent for as long
as nothing subclasses WP_Theme_JSON. The moment something does, `static::`
resolves against a class where the method is not visible.

Nothing in core extends WP_Theme_JSON today, so this is latent rather than an
active defect. It is worth correcting regardless: `self::` is what a private
method actually means, and the class is a plausible extension point, being
the one Gutenberg mirrors.

The methods involved are sanitize_viewport_settings(),
is_valid_viewport_breakpoint_size(), get_viewport_breakpoint_value_in_pixels(),
update_paragraph_text_indent_selector(), update_button_width_declarations(),
get_block_name_from_metadata_path(), and get_feature_selector(). All were
introduced after 7.0.

Regenerating the baselines drops 7 entries covering 14 errors from
tests/phpstan/baselines/staticClassAccess.privateMethod.neon. The remaining
entries in that file are unrelated to this class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The method has been annotated `@param object` for both `$metadata` and `$node`
since 6.3.0, but it has only ever been passed arrays, and only ever treats them
as arrays: it reads `$metadata['selectors']` and `$node[ $feature ]`, builds
`$node[ $feature ][ $subfeature ]`, and unsets through the reference. Given a
real object, the first subscript would be fatal.

The wrong type propagated. Because `$node` is taken by reference, callers had
their own variable narrowed to object after the call, so the same node then
reported the mirror-image error when handed to process_pseudo_selectors(),
which correctly documents `array`. Code added since 7.0 in
get_styles_for_block() made that visible in three places.

Correcting the two annotations resolves all five errors the file reported,
without touching a line of executable code, and documents that features
promoted to their own selector are removed from `$node`.

Regenerating the baselines drops 3 entries covering 5 errors from
tests/phpstan/baselines/argument.type.neon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shape-mismatch guard tested `array() !== $current` after already requiring
`! array_is_list( $current )`. Since array_is_list() returns true for an empty
array, the negation cannot hold unless $current is non-empty, so the trailing
comparison was always true and never decided anything.

Only that clause is removed. The matching test on $incoming stays, because it
is live: $incoming is a list at that point and may legitimately be empty, which
is the documented exemption letting replace() clear a list. So does the similar
check further down, where array_is_list( $current ) is asserted rather than
negated and an empty array therefore still reaches it.

Behaviour is unchanged. The 78 view config tests pass.

Regenerating the baselines drops 1 entry from
tests/phpstan/baselines/notIdentical.alwaysTrue.neon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two guards added since 7.0 tested isset() on a declared property that has a
non-null default and is therefore never unset:

* WP_Posts_List_Table::get_primary_column_aria_label() paired
  isset( $item->post_title ) with ! empty( $item->post_title ). WP_Post declares
  $post_title as a string, and empty() already covers unset, null and '', so the
  isset() decided nothing.
* wp_get_block_state_style_rules() paired isset( $block_type->selectors ) with
  is_array( $block_type->selectors ). WP_Block_Type declares
  `public $selectors = array()`.

Only the isset() is removed in each case. The is_array() test on $selectors
stays: the property carries no native type, so a plugin can assign a non-array
to it, and that check is doing real work.

Behavior is unchanged. The 46 block supports states tests pass.

Regenerating the baselines drops 2 entries from
tests/phpstan/baselines/isset.property.neon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The delete-users confirmation block queries $wpdb for a user's posts and links
without the file ever importing the global. It works, since a top-level admin
page runs at global scope, but nothing said so, and static analysis reported
the variable as possibly undefined.

Import it with the `@global` docblock core uses elsewhere for the same purpose,
matching edit.php and edit-comments.php. The tag alone is not enough: the
PHPStan visitor bridging core's `@global` tags acts on `global` statements, so
the statement carries the type and the tag documents it.

Two of the three reports on the file are on lines added since 7.0; the third
predates it and is fixed by the same declaration.

Regenerating the baselines drops the users.php entry from
tests/phpstan/baselines/variable.undefined.neon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The HTML API passes null for substr_compare()'s $length, which has always meant
"compare the full length". PHP 7.4 spelled the parameter `int $length = null`,
where the null default makes it implicitly nullable, and PHP 8.0 only made that
explicit as `?int $length = null`. The behavior never differed; running the call
on PHP 7.4 confirms null compares the whole string rather than coercing to a
length of 0, which is the failure that would matter here.

PHPStan reads the two spellings from two sources and only one is right. Its PHP
8 stub carries `?int`, but the pre-8.0 resources/functionMap.php records plain
`int`, having dropped the implicit nullability when it was transcribed from the
old manual. Since phpVersion.min is 70400, the legacy map wins and null is
reported as invalid.

Move the entry out of the baseline and into ignoreErrors, where the surrounding
comment records all of the above. A baseline entry is a promise to fix
something, and there is nothing here to fix: the call is correct on every
version WordPress supports, and passing an explicit length purely to satisfy the
analyzer would change working code to suit a tooling bug. reportUnmatched is
false so the entry lapses quietly once PHPStan corrects its map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both are user preferences read straight off a WP_User through the magic
__get(), exactly like rich_editing and syntax_highlighting beside them, but
neither was listed among the class's @Property tags. Nothing could resolve
them, so every read was unverifiable.

That produced a misleading report on the profile screen. user-edit.php reads
43 properties off $profile_user, which is WP_User|false because
get_user_to_edit() returns get_userdata() unchanged. Access on a union is
only reported from level 7, so at level 5 the other 41 reads passed on the
strength of their @Property tags, and only these two, which resolved to
nothing, fell through to a complaint about the union. The message named the
union, but the union was not the cause: adding a guard for the false case
merely turned each into "access to an undefined property" on the same line.
Documenting the two properties resolves them with no guard at all.

The false case is unreachable in any event. The screen already dies with
"Invalid user ID." when get_userdata() rejects $user_id, and $user_id is not
reassigned between that check and the call, so the user provably exists.
PHPStan cannot connect a guard phrased in terms of get_userdata() to a later
call to get_user_to_edit().

infinite_scrolling arrived after 7.0 with the Media Library option;
comment_shortcuts long predates it and was missing for the same reason.

Regenerating the baselines drops 2 entries from
tests/phpstan/baselines/property.nonObject.neon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The method proxies four kinds of value: get_variations() gives array[],
get_uses_context() gives string[], a handles property with more than one entry
gives string[], and a single entry gives a string, with null for anything it
does not recognise. The documented return listed only string|string[]|null, so
the array[] from the variations branch was never covered.

That branch has been there since 6.5.0, but the omission was invisible while
void sat in the union: PHPStan takes void to mean the method may return
nothing and does not hold the remaining types to account. r62178 dropped void
in favour of null, which turned the union into a contract and surfaced the gap
immediately.

This is the second such report from that commit. The first was
WP_Widget::form(), where the correct fix was to restore void because the
subclasses genuinely return nothing. Here void was misleading, since every path
returns a value, so the fix is to finish the union rather than reinstate it.

Behaviour is unchanged. The 104 block type tests pass.

Regenerating the baselines drops 1 entry from
tests/phpstan/baselines/return.type.neon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
r62680 gave term_exists() a conditional return type, and the branch for an
empty $taxonomy says int|null. The function returns a string there.

Without a taxonomy the query keeps its default 'fields' => 'ids', so the value
shifted off the result set is a term ID, and the function hands it back as
`return (string) $_term;`. Only when a taxonomy is passed does 'fields' become
'all' and the array of term_id and term_taxonomy_id get returned instead. The
prose above the tag says "Returns the term ID", which is silent on int versus
string; the conditional type resolved that silence the wrong way.

Say numeric-string|null for that branch. The error this clears is the analyzer
correctly reporting the function against its own declared type, not a fault in
the function.

Nothing downstream depended on the incorrect int: a full run before
regenerating the baselines reported no new errors anywhere, only the stale
entry for this one. The 26 term_exists tests pass.

Regenerating the baselines drops 1 entry from
tests/phpstan/baselines/return.type.neon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
r62822 typed $comment_ID and $comment_post_ID as numeric-string, which is what
hydrating a comment from the database produces. One place in core then
contradicts it: get_comment_to_edit() casts both to int and writes them back
onto the same object, so from that point on the object no longer matches its
own declared type.

Widen both to numeric-string|int and say in the description which function does
it. The alternative, dropping the casts in get_comment_to_edit(), would change
what its callers have received for years, and this is the same hedge already
applied to WP_User::$user_level, documented as int|numeric-string|''.

The narrower type was also costing precision rather than buying it. Because the
analysis runs with treatPhpDocTypesAsCertain disabled, a union carrying int
satisfies the many core call sites that hand these properties to parameters
typed int, all of which had been baselined as string given. Regenerating drops
39 entries across 9 files, from comment.php and the REST controller to the
recent comments widget, and introduces nothing: a full run before regenerating
reported no new errors anywhere.

The 794 comment tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 16, 2026

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.

Core Committers: Use this line as a base for the props when committing in SVN:

Props westonruter, apermo.

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

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The 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

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Three conflicts, resolved as follows:

- `src/wp-includes/class-wp-block-type.php`: trunk removed the stray blank
  line between `@param` and `@return` in `__get()`'s docblock while this
  branch widened the `@return` type. Both changes kept.
- `tests/phpstan/baselines/argument.type.neon` and
  `tests/phpstan/baselines/return.missing.neon`: taken from trunk, then all
  baselines regenerated with `composer phpstan:baselines` against the merged
  tree. Regeneration drops 41 `argument.type` entries that the widened
  `WP_Comment` ID properties now satisfy.

`WP_Widget::form()`'s `string|void` return has since landed on trunk
independently, so this branch's commit for it is now a no-op.

`vendor/bin/phpstan analyse --configuration=phpstan.neon.dist` reports no
errors on the merged tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pento pushed a commit that referenced this pull request Aug 28, 2026
…es on `WP_User`.

Both are user preferences read straight off a `WP_User` through the magic `__get()`, exactly like `rich_editing` and `syntax_highlighting` beside them, but neither was listed among the class's `@property` tags, so nothing could resolve them. The `infinite_scrolling` property arrived in r62632 with the Media Library opt-out option; `comment_shortcuts` has gone undocumented since r9217 added the comment hotkeys opt-in, and was missing for the same reason. Regenerating the baselines drops both entries from `tests/phpstan/baselines/property.nonObject.neon`.

Developed as subset of #13064.
Follow-up to r9217, r62632, r63020.

See #65817.


git-svn-id: https://develop.svn.wordpress.org/trunk@63383 602fd350-edb4-49c9-b593-d223f7449a82
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Aug 28, 2026
…es on `WP_User`.

Both are user preferences read straight off a `WP_User` through the magic `__get()`, exactly like `rich_editing` and `syntax_highlighting` beside them, but neither was listed among the class's `@property` tags, so nothing could resolve them. The `infinite_scrolling` property arrived in r62632 with the Media Library opt-out option; `comment_shortcuts` has gone undocumented since r9217 added the comment hotkeys opt-in, and was missing for the same reason. Regenerating the baselines drops both entries from `tests/phpstan/baselines/property.nonObject.neon`.

Developed as subset of WordPress/wordpress-develop#13064.
Follow-up to r9217, r62632, r63020.

See #65817.

Built from https://develop.svn.wordpress.org/trunk@63383


git-svn-id: http://core.svn.wordpress.org/trunk@62576 1a063a9b-81f0-0310-95a4-ce76da25c4cd
No conflicts. Trunk advanced by two revisions:

- r63382 "Editor: Add the fit text CSS class to server-rendered blocks", which
  this branch does not touch. It is the whole content of the merge.
- r63383 "Users: Document `comment_shortcuts` and `infinite_scrolling`
  properties on `WP_User`", which is this branch's own commit cbcdd48 landed
  to trunk. It contributes no delta, since the two are content-identical.

`vendor/bin/phpstan analyse --configuration=phpstan.neon.dist` reports no errors
on the merged tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pento pushed a commit that referenced this pull request Aug 28, 2026
In r52744 this class's internal calls were rewritten from `self::` to `static::` so that the subclasses it was opening the class up to could override them. For a private method that buys nothing, since a subclass cannot supply one, and PHPStan reports it as unsafe: late static binding resolves `static::` to the runtime class, where the private method is not visible. Thirty such call sites were left, reaching fourteen private statics. All fourteen are declared `private static` on `WP_Theme_JSON` itself, so `self::` is behavior-identical. A docblock example on `compute_spacing_sizes()` showed the same form and is updated along with the code it documents.

This now eliminates the `tests/phpstan/baselines/staticClassAccess.privateMethod.neon` baseline. In r63368 the identifier was cleared from the four other classes reporting it, and `WP_Theme_JSON` was the last.

Developed as subset of #13064.
Follow-up to r52744, r63020, r63368.

See #65817.


git-svn-id: https://develop.svn.wordpress.org/trunk@63384 602fd350-edb4-49c9-b593-d223f7449a82
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Aug 28, 2026
In r52744 this class's internal calls were rewritten from `self::` to `static::` so that the subclasses it was opening the class up to could override them. For a private method that buys nothing, since a subclass cannot supply one, and PHPStan reports it as unsafe: late static binding resolves `static::` to the runtime class, where the private method is not visible. Thirty such call sites were left, reaching fourteen private statics. All fourteen are declared `private static` on `WP_Theme_JSON` itself, so `self::` is behavior-identical. A docblock example on `compute_spacing_sizes()` showed the same form and is updated along with the code it documents.

This now eliminates the `tests/phpstan/baselines/staticClassAccess.privateMethod.neon` baseline. In r63368 the identifier was cleared from the four other classes reporting it, and `WP_Theme_JSON` was the last.

Developed as subset of WordPress/wordpress-develop#13064.
Follow-up to r52744, r63020, r63368.

See #65817.

Built from https://develop.svn.wordpress.org/trunk@63384


git-svn-id: http://core.svn.wordpress.org/trunk@62577 1a063a9b-81f0-0310-95a4-ce76da25c4cd
One conflict. Trunk advanced by one revision, r63384, which is this branch's own
commit 97f1544 landed and extended: it converted all fourteen of
WP_Theme_JSON's private statics to self::, where the branch had converted the
seven that changed after 7.0.0. That emptied
tests/phpstan/baselines/staticClassAccess.privateMethod.neon, so trunk deleted
the file while the branch had only trimmed it - a modify/delete conflict,
resolved by taking trunk's deletion.

The result absorbs the branch's version entirely. No static:: call to a private
method remains in the class, and the only difference from trunk in
class-wp-theme-json.php is commit 5c6a81b's docblock, which is a separate
concern still under review here.

vendor/bin/phpstan analyse --configuration=phpstan.neon.dist reports no errors
on the merged tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
// A non-empty list only lands where a list (or nothing) lives, under
// merge() and replace() alike. An empty array is shape-ambiguous and
// exempt, so replace() with an empty list can still clear a list.
if ( array() !== $incoming && is_array( $current ) && ! array_is_list( $current ) && array() !== $current ) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why remove that? I don't see how this is not a different behavior.

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.

The && array() !== $current condition is flagged as a PHPStan error:

phpstan: Strict comparison using !== between array{} and non-empty-array<mixed, mixed> will always evaluate to true.

Thinking this through: If $current is an array but it is not a list, then is it guaranteed to never be an empty array as PHPStan has identified?

@westonruter westonruter Aug 28, 2026

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.

Because if it is not a list, then it must have a non-sequential key, and this implies that the array is not empty?

Also per Claude Opus 5:

Not a behavior change — array_is_list( array() ) returns true, so an empty array is already excluded by ! array_is_list( $current ). Anything reaching that point is necessarily a non-empty array, which is why PHPStan flags the array() !== $current comparison as always true. The array() !== $incoming check earlier in the condition is still load-bearing; only the $current half was redundant.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yeah agreed, can follow you on that now. was not fully aware of array_is_list()

@apermo apermo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

pento pushed a commit that referenced this pull request Sep 1, 2026
… the 7.1 cycle.

These are the errors trunk reports that a baseline generated from 7.0.0's `src` does not, restricted to those where the symbol whose type provoked the error was itself changed after the 7.0 tag.

Four annotations are corrected:

 * On `WP_Comment`, `$comment_ID` and `$comment_post_ID` widen to `numeric-string|int`, since the `numeric-string` added in r62640 is contradicted by `get_comment_to_edit()`, which has replaced both with integers in place since long before.
 * On `term_exists()`, the conditional return added in r62680 promises `int|null` for the empty-`$taxonomy` branch, which actually returns a cast string.
 * On `WP_Block_Type::__get()`, the union narrowed in r62178 omits the arrays returned for the `variations` and `uses_context` names.
 * On `WP_Theme_JSON::get_feature_declarations_for_node()`, both parameters have been annotated `object` since r56058 but only ever receive arrays, and the by-reference `$node` propagated the wrong type back to callers.

The remaining four are code rather than annotations:

 * The users screen gains the `$wpdb` global that the queries added in r62688 read without importing.
 * An `isset()` check on `$block_type->selectors` is dropped, since the property is declared with a non-null default in r62453.
 * An `isset()` check paired with `! empty()` is dropped in the posts list table, where r62838 left both guarding the same property.
 * In `WP_View_Config_Data::merge_properties()`, an `array() !== $current` check is redundant after `! array_is_list( $current )`, which already implies non-empty.

One report is suppressed rather than fixed: PHPStan reads `substr_compare()`'s `$length` from its pre-8.0 `functionMap.php`, which dropped the parameter's implicit nullability, so a correct call is reported as invalid on every version WordPress supports. That belongs in `ignoreErrors` rather than a baseline, since a baseline entry records work still to be done and there is none.

Baselines are regenerated, clearing 49 entries and 81 errors across six files.

Developed in #13064.
Follow-up to r56058, r62178, r62453, r62640, r62680, r62688, r62834, r62838, r63383, r63384.

See #65817.


git-svn-id: https://develop.svn.wordpress.org/trunk@63426 602fd350-edb4-49c9-b593-d223f7449a82
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Sep 1, 2026
… the 7.1 cycle.

These are the errors trunk reports that a baseline generated from 7.0.0's `src` does not, restricted to those where the symbol whose type provoked the error was itself changed after the 7.0 tag.

Four annotations are corrected:

 * On `WP_Comment`, `$comment_ID` and `$comment_post_ID` widen to `numeric-string|int`, since the `numeric-string` added in r62640 is contradicted by `get_comment_to_edit()`, which has replaced both with integers in place since long before.
 * On `term_exists()`, the conditional return added in r62680 promises `int|null` for the empty-`$taxonomy` branch, which actually returns a cast string.
 * On `WP_Block_Type::__get()`, the union narrowed in r62178 omits the arrays returned for the `variations` and `uses_context` names.
 * On `WP_Theme_JSON::get_feature_declarations_for_node()`, both parameters have been annotated `object` since r56058 but only ever receive arrays, and the by-reference `$node` propagated the wrong type back to callers.

The remaining four are code rather than annotations:

 * The users screen gains the `$wpdb` global that the queries added in r62688 read without importing.
 * An `isset()` check on `$block_type->selectors` is dropped, since the property is declared with a non-null default in r62453.
 * An `isset()` check paired with `! empty()` is dropped in the posts list table, where r62838 left both guarding the same property.
 * In `WP_View_Config_Data::merge_properties()`, an `array() !== $current` check is redundant after `! array_is_list( $current )`, which already implies non-empty.

One report is suppressed rather than fixed: PHPStan reads `substr_compare()`'s `$length` from its pre-8.0 `functionMap.php`, which dropped the parameter's implicit nullability, so a correct call is reported as invalid on every version WordPress supports. That belongs in `ignoreErrors` rather than a baseline, since a baseline entry records work still to be done and there is none.

Baselines are regenerated, clearing 49 entries and 81 errors across six files.

Developed in WordPress/wordpress-develop#13064.
Follow-up to r56058, r62178, r62453, r62640, r62680, r62688, r62834, r62838, r63383, r63384.

See #65817.

Built from https://develop.svn.wordpress.org/trunk@63426


git-svn-id: http://core.svn.wordpress.org/trunk@62614 1a063a9b-81f0-0310-95a4-ce76da25c4cd
@westonruter westonruter closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants