Make WordPress Core

Opened 5 months ago

Closed 2 weeks ago

Last modified 4 days ago

#64898 closed task (blessed) (fixed)

PHPStan code quality improvements for 7.1

Reported by: desrosj Owned by:
Priority: normal Milestone: 7.1
Component: General Version:
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses: coding-standards

Description

This ticket is for various code quality issues and improvements surfaced via PHPStan.

Implementing PHPStan is tracked separately in #61175.

Previously:

Change History (99)

This ticket was mentioned in PR #11302 on WordPress/wordpress-develop by @Soean.


5 months ago
#1

  • Keywords has-patch added

This pull request makes a small change to the check_import_new_users function, simplifying its logic to directly return the result of the permission check.

Trac ticket: https://core.trac.wordpress.org/ticket/64898

This ticket was mentioned in PR #11303 on WordPress/wordpress-develop by @Soean.


5 months ago
#2

The url value is $tag->link unless '#' === $tag->link, then its #. Which is the same as $tag->link.
So we can simplify it and always use $tag->link.

Trac ticket: https://core.trac.wordpress.org/ticket/64898

#3 @SergeyBiryukov
5 months ago

In 62066:

Code Quality: Simplify tag URL assignment in wp_generate_tag_cloud().

This removes a redundant ternary that no longer affects the logic after the esc_url() call on the tag URL was moved to the output in an earlier revision.

Follow-up to [9518], [11383], [32996].

Props Soean.
See #64898.

@SergeyBiryukov commented on PR #11303:


5 months ago
#4

Thanks for the PR! Merged in r62066.

@SergeyBiryukov commented on PR #11302:


5 months ago
#5

Thanks for the PR! Merged in r62086.

This ticket was mentioned in PR #11340 on WordPress/wordpress-develop by @Soean.


5 months ago
#6

This pull request makes minor code cleanups across several files, removing unnecessary uses of the sprintf function when localizing or outputting static strings. These changes simplify the code and improve readability without affecting functionality.

Introduced in:

Trac ticket: https://core.trac.wordpress.org/ticket/64898

#7 @SergeyBiryukov
5 months ago

In 62167:

Code Quality: Unwrap sprintf() with one argument.

This removes unnecessary uses of the sprintf() function when localizing or outputting static strings. These changes simplify the code and improve readability without affecting functionality.

Props Soean.
See #64898.

@SergeyBiryukov commented on PR #11340:


5 months ago
#8

Thanks for the PR! Merged in r62167.

This ticket was mentioned in PR #11331 on WordPress/wordpress-develop by @Soean.


5 months ago
#9

This pull request makes minor code simplifications by removing unnecessary ternary operations and directly assigning boolean expressions. These changes make the code easier to read and maintain, but do not alter the underlying logic.

Trac ticket: https://core.trac.wordpress.org/ticket/64898

#10 @SergeyBiryukov
5 months ago

In 62173:

Code Quality: Simplify boolean assignments.

This makes minor code simplifications by removing unnecessary ternary operations and directly assigning boolean expressions. These changes make the code easier to read and maintain, but do not alter the underlying logic.

Props Soean.
See #64898.

@SergeyBiryukov commented on PR #11331:


5 months ago
#11

Thanks for the PR! Merged in r62173.

@Soean commented on PR #11429:


5 months ago
#13

Maybe we can use the PHPStan ticket: https://core.trac.wordpress.org/ticket/64898

#14 @SergeyBiryukov
5 months ago

In 62201:

Code Quality: Remove unused variable in WP_Block_Patterns_Registry.

Follow-up to [56805], [59101].

Props Soean, mukesh27.
See #64898.

@westonruter commented on PR #11429:


5 months ago
#15

This was committed by @SergeyBiryukov in r62201 (8510818).

This ticket was mentioned in PR #11692 on WordPress/wordpress-develop by @westonruter.


4 months ago
#16

  • Keywords has-unit-tests added

tl;dr: Adds a PHPStan parser-node visitor that bridges WordPress core's @global Type $varname PHPDoc convention to PHPStan's variable type resolution, eliminating ~3,800 false-positive mixed errors without touching any production code.


I use a local PHPStan configuration at level 10. This causes PHPStan to complain a _lot_, including about the use of globals like $wpdb. For code like this:

https://github.com/WordPress/wordpress-develop/blob/75b41314907d43bb111e22e305ba048bd4b90ec2/src/wp-includes/class-wp-network-query.php#L387

I get two errors from PHPStan:

  • phpstan: Cannot access property $site on mixed.
  • phpstan: Part $wpdb->site (mixed) of encapsed string cannot be cast to string.

This is in spite of the fact that the method has:

https://github.com/WordPress/wordpress-develop/blob/75b41314907d43bb111e22e305ba048bd4b90ec2/src/wp-includes/class-wp-network-query.php#L322

and

https://github.com/WordPress/wordpress-develop/blob/75b41314907d43bb111e22e305ba048bd4b90ec2/src/wp-includes/class-wp-network-query.php#L327

The issue is that PHPStan doesn't recognize this use of @global. It's a WordPress-specific thing. It does recognize this, however:

/** @var wpdb $wpdb */
global $wpdb;

While we could fix the issues by going throughout core and adding these @var tags, this would be extremely noisy and be of very little value. Instead, we can introduce a PHPStan extension for core which causes PHPStan to treat the @global annotations as aliases for inline @var annotations. This is what is implemented by this PR.

# Approach by Claude

Adds a custom parser node visitor, WordPress\PHPStan\GlobalDocBlockVisitor, that:

  1. Walks each FunctionLike node and parses any @global Type $name tags from its docblock.
  2. For every global $name; statement inside that function body, if $name matches a documented tag, attaches a synthetic /** @var Type $name */ doc comment to the global AST node.
  3. PHPStan's existing @var-on-global handling then assigns the documented type.

Behavior:

  • Functions without @global tags are untouched; their globals continue to resolve as mixed.
  • Globals not listed in the function's @global block are untouched; they continue to resolve as mixed.
  • Hand-written @var annotations already present on a global statement are respected and preserved.
  • Nested functions/closures get their own @global map (visitor uses a stack).

The visitor is registered as a phpstan.parser.richParserNodeVisitor service in tests/phpstan/base.neon and autoloaded via a new autoload-dev PSR-4 entry in composer.json mapping WordPress\PHPStan\ to tests/phpstan/. composer install (which runs composer dump-autoload) makes the class available to PHPStan automatically.

# Result

When running composer -- phpstan --configuration=phpstan.neon.dist --level=10:

  • Before: [ERROR] Found 40069 errors
  • After: [ERROR] Found 36300 errors

## Claude comparison

### Comparison summary

Trunk Branch Diff
------------------------------------------- ----: -----: --------:
Total errors (PHPStan totals) 40069 36300 -3769
Unique errors (file+line+id+msg) 39372 35705 -3667
Fixed by branch (in trunk, not branch) 4456
Introduced by branch (in branch, not trunk) 789

The fixed-vs-introduced asymmetry exists because the visitor narrows $wpdb etc. from mixed to wpdb, which both removes errors (the mixed.method() family) and exposes new ones (real type mismatches in wpdb method signatures, @var array injections that PHPStan wants array<...> for, etc.).

### Top fixed identifiers

1375 method.nonObject              -- $foo->method() on what was mixed
1040 property.nonObject            -- $foo->prop on what was mixed
 607 encapsedStringPart.nonString  -- "FROM $wpdb->posts" interpolations
 462 argument.type
 304 offsetAccess.nonOffsetAccessible
 188 binaryOp.invalid
 126 foreach.nonIterable
 119 return.type
  64 cast.int
  46 assignOp.invalid
  39 property.notFound
  32 method.notFound
  21 offsetAccess.invalidOffset
  15 assign.propertyType
   6 preInc.type

### Top fixed messages

304 Cannot call method prepare() on mixed.
233 Cannot access property $posts on mixed.
188 Part $wpdb->posts (mixed) of encapsed string cannot be cast to string.
126 Argument of an invalid type mixed supplied for foreach, only iterables are supported.
111 Cannot access offset string on mixed.
110 Cannot call method query() on mixed.
108 Cannot call method get_results() on mixed.
100 Cannot call method get_var() on mixed.
 82 Cannot access offset mixed on mixed.
 75 Cannot access property $comments on mixed.
 64 Cannot cast mixed to int.
 64 Cannot call method get_col() on mixed.
 59 Cannot call method update() on mixed.
 53 Cannot access property $options on mixed.
 52 Part $wpdb->comments (mixed) of encapsed string cannot be cast to string.
 51 Cannot call method delete() on mixed.
 44 Cannot access property $term_taxonomy on mixed.
 41 Cannot call method esc_like() on mixed.
 38 Cannot access property $postmeta on mixed.
 38 Part $wpdb->options (mixed) of encapsed string cannot be cast to string.
 27 Cannot access property $site on mixed.    ← your original case

The 4456 fixed errors all stem from globals (mostly $wpdb, $wp_query, $wp_locale, $wp_filter, etc.) becoming concretely typed where @global tags exist. Nothing was suppressed via baseline; these are real PHPStan errors that the visitor now resolves.

Trac ticket: https://core.trac.wordpress.org/ticket/64898

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Opus 4.7
Used for: Research and code writing

@westonruter commented on PR #11692:


4 months ago
#17

cc @justlevine, @apermo, @SergeyBiryukov

@westonruter commented on PR #11692:


4 months ago
#18

cc @szepeviktor

@westonruter commented on PR #11692:


4 months ago
#19

With 9fc7599, the error count is now brought down by another _three_ to 36,297 (from 36,300).

@szepe.viktor commented on PR #11692:


4 months ago
#20

@westonruter Please do not kill my package.

@westonruter commented on PR #11692:


4 months ago
#21

@szepeviktor Not my intention! 😅

If anything, this new PHPStan extension would reduce the need for php-stubs/wordpress-stubs. But it's just here for core. It seems like adding such @global support would be useful in your extension for themes and plugins to leverage?

@westonruter commented on PR #11692:


4 months ago
#22

With 38f2bf2, the total error count is brought down by another dozen to 36,285 (from 36,297). The regex was overengineered.

@szepe.viktor commented on PR #11692:


4 months ago
#23

It seems like adding such @global support would be useful in your extension for themes and plugins to leverage?

No. Some day WordPress will use PHP standards.

@westonruter commented on PR #11692:


4 months ago
#24

No. Some day WordPress will use PHP standards.

But wouldn't that kill your package? 😏

#25 @westonruter
4 months ago

In 62292:

Build/Test Tools: Honor @global docblock tags in PHPStan analysis.

This adds a PHPStan extension with a parser-node visitor that bridges WordPress core's @global Type $varname PHPDoc convention to PHPStan's variable type resolution, eliminating 3,784 spurious errors caused by globals resolving as mixed when on rule level 10 (out of 40,069 errors total, so a 9.4% reduction). This avoids the need to add /** @var Type $varname */ annotations with each global variable.

Developed in https://github.com/WordPress/wordpress-develop/pull/11692

Props westonruter, apermo, szepeviktor.
See #64898.

@westonruter commented on PR #11692:


4 months ago
#26

Committed in r62292 (9e0b63d).

#27 @westonruter
3 months ago

In 62408:

Plugins: Improve hook performance by using spl_object_id() instead of spl_object_hash() to construct unique IDs.

  • Also use spl_object_id() similarly when registering and unregistering classic widgets.
  • Improve typing and phpdoc in _wp_filter_build_unique_id(). Return null for malformed callbacks.
  • Add tests for _wp_filter_build_unique_id().
  • Improve type safety of WP_Hook::add_filter() in case an invalid callback is provided for parity with ::has_filter() and ::remove_filter().

Developed in https://github.com/WordPress/wordpress-develop/pull/11865
Follow-up to r46220, r46801, r60179.

Props bor0, westonruter, SergeyBiryukov, schlessera, arshidkv12, knutsp, spacedmonkey, swissspidy.
See #64898.
Fixes #58291.

This ticket was mentioned in PR #11979 on WordPress/wordpress-develop by @huzaifaalmesbah.


3 months ago
#28

Resolves PHPStan level 1 variable.undefined errors in four user/plugin admin files (98 of 564 project-wide level-1 errors). The project's enforced level (0) continues to pass.

  • Declare @global docblocks + global statements for core globals ($wpdb, $current_user, $wp_roles, $_wp_admin_css_colors, $status, $page, $plugins, $user_ID, $usersearch, $blog_id).
  • Initialize $redirect in wp-admin/users.php for the empty( $_REQUEST ) branch.
  • Initialize $user_id, $old_user_data at the top of wp_insert_user() and $manage_url in _wp_privacy_send_request_confirmation_notification() so they're always defined.
  • Bug fix: wp-admin/user-edit.php:166 referenced an undefined $user_login in the multisite signup-email update query; corrected to $user->user_login.

## Files changed

File Errors before (level 1) After
src/wp-admin/plugins.php 40 0
src/wp-admin/users.php 25 0
src/wp-admin/user-edit.php 15 0
src/wp-includes/user.php 18 0

## Verification

vendor/bin/phpstan analyse --level=1 --memory-limit=2G \
  --configuration=phpstan.neon.dist \
  src/wp-admin/plugins.php \
  src/wp-admin/users.php \
  src/wp-admin/user-edit.php \
  src/wp-includes/user.php

### Expected: [OK] No errors

#29 @westonruter
3 months ago

In 62437:

Privacy: Correct type of WP_User_Request::$user_id from int to string/numeric-string.

Also adds numeric-string as richer PHPStan type to WP_Post::$post_author and WP_Post::$comment_count.

Developed in https://github.com/WordPress/wordpress-develop/pull/12018.
Follow-up to r25086, r43011.

Props masteradhoc, desrosj, garrett-eclipse, johnbillion, westonruter, apermo, SergeyBiryukov, TZ-Media, andizer, javorszky.
See #22324, #25092, #43443, #43985, #64898.
Fixes #44723.

@noruzzaman commented on PR #11979:


3 months ago
#30

Nice work! I noticed that src/wp-admin/user-new.php has similar undefined variable issues under PHPStan level 1 (e.g., $blog_id on line 68 and $wpdb on line 243).

Would it be possible to include it in this PR as well?

#31 @westonruter
2 months ago

In 62488:

Cron: Add type definitions to private cron functions.

This addresses PHPStan rule level 10 errors with these functions:

  • _get_cron_array()
  • _set_cron_array()
  • _upgrade_cron_array()

See #64898.

This ticket was mentioned in PR #12158 on WordPress/wordpress-develop by @westonruter.


2 months ago
#32

Trac ticket: https://core.trac.wordpress.org/ticket/64898

## Use of AI Tools

None

#33 @westonruter
2 months ago

In 62495:

Build/Test Tools: Upgrade PHPStan to version 2.2.2.

Developed in https://github.com/WordPress/wordpress-develop/pull/12158.

See #64898.

#34 @westonruter
2 months ago

In 62520:

Editor: Fix wp-elements-* CSS class name collisions for identical blocks.

The wp_get_elements_class_name() function previously generated CSS class names by hashing the serialized block data via md5(). Identical blocks received the same wp-elements-* class name and the Style Engine deduplicated their CSS rules into one, causing a parent block's element style (e.g. link color) to cascade down and override a child block's identical style due to CSS source order.

The function is updated to use wp_unique_prefixed_id() instead, generating sequential unique class names (wp-elements-1, wp-elements-2, etc.) that match the block editor's JavaScript implementation. The now-unused $parsed_block parameter is removed from the function signature.

PHPStan rule level 10 errors are also resolved in the related code. See #64898.

Developed in https://github.com/WordPress/wordpress-develop/pull/12126.
Follow-up to r53260, r58074.

Props tusharbharti, westonruter, wildworks.
Fixes #65435.

#35 @westonruter
2 months ago

In 62529:

Docs: Clarify return value semantics of wpdb query methods.

This eliminates over 400 PHPStan errors from the core codebase.

  • Clarify the inline documentation for the four wpdb query methods — get_results(), get_row(), get_col(), and get_var().
  • Add @phpstan-return conditional types that mirror each method's runtime dispatch on $query and $output.
  • Add @phpstan-param tags narrowing $output to the documented constants.
  • Document that get_var() returns null both on failure and when the matched cell value is an empty string, directing consumers to $this->last_error to distinguish the two cases.
  • Tighten the @return in get_results() from array|object|null to array|null, since the method never returns a bare stdClass; the object was a copy/paste artifact from get_row().
  • Fix a deprecated use of null as an array offset (PHP 8.5) in the OBJECT_K branch when a row's first column is SQL NULL.
  • Gather get_col() data as a true list.
  • Suggest ext-mysqli in composer.json, which wpdb requires at runtime.

Developed in https://github.com/WordPress/wordpress-develop/pull/11855.

Props apermo, westonruter.
See #30257, #64898.
Fixes #65261.

This ticket was mentioned in PR #12405 on WordPress/wordpress-develop by @westonruter.


7 weeks ago
#36

Trac ticket: https://core.trac.wordpress.org/ticket/64898

## Use of AI Tools

n/a

#37 @westonruter
7 weeks ago

In 62635:

Docs: Modernize and improve specificity of types in WP_Error class.

This brings the WP_Error class to full PHPStan rule level 10 compliance.

Also make use of null coalescing operator where appropriate, and simplify has_errors() method.

Developed in https://github.com/WordPress/wordpress-develop/pull/12405.
Follow-up to r42761, r49115, r49116.

See #64898, #64897.

#38 @westonruter
7 weeks ago

In 62637:

Filesystem API: Improve type safety across the transport classes.

Change the optional constructor argument of WP_Filesystem_FTPext, WP_Filesystem_ftpsockets, and WP_Filesystem_SSH2 from an empty string default to an empty array, matching how the argument is actually consumed, and improve the associated DocBlocks.

These classes were also brought to adherence with PHPStan rule level 10:

  • Add FileListing and Options array shapes, and initialize each transport's $options to a complete default array before any early return.
  • Correct several inaccurate @return descriptions, including the group() methods that had been describing the owner.
  • Allow WP_Filesystem_SSH2::connect() to be retried after a failed connection attempt.
  • Stop WP_Filesystem_FTPext::parselisting() from leaking its intermediate date-parsing keys into the returned listing.
  • Add ext-ftp and ext-ssh2 to the suggested extensions in composer.json.

Developed in https://github.com/WordPress/wordpress-develop/pull/11593.
Follow-up to r62635, r62636.

Props soean, westonruter, mukesh27.
See #65584, #64898.
Fixes #65409.

This ticket was mentioned in PR #12407 on WordPress/wordpress-develop by @westonruter.


6 weeks ago
#39

The GlobalDocBlockVisitor PHPStan extension (introduced in 9e0b63d872) only read @global tags from the enclosing function's docblock, and skipped global statements outside any function entirely. As a result, file-scope global statements (e.g. in admin templates like wp-admin/edit-form-comment.php that are included into another scope) required a redundant @var tag in addition to @global for PHPStan to resolve the variable's type:

/**
 * @global WP_Comment $comment Global comment object.
 * @var WP_Comment $comment
 */
global $comment;

With this change, @global tags in a docblock attached directly to a global statement are honored as well, so the @var tag above is no longer needed. Statement-level tags take precedence over the enclosing function's tags for the same variable, and handwritten @var annotations continue to win over synthetic ones.

Verified by running PHPStan against src/wp-admin/edit-form-comment.php with the redundant @var removed: all Cannot access property ... on mixed errors for $comment are resolved by the @global tag alone.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Fable 5
Used for: Diagnosing the visitor's file-scope gap and implementing the fix; reviewed, tested, and verified by me.

---
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.

🤖 Generated with Claude Code

This ticket was mentioned in PR #12408 on WordPress/wordpress-develop by @westonruter.


6 weeks ago
#40

Follow-up to [62437] which corrected the type of WP_User_Request::$user_id and added numeric-string as a richer PHPStan type to WP_Post::$post_author and WP_Post::$comment_count. This PR extends the same treatment to the remaining DB-backed class properties which are documented as string but always hold numeric strings, and corrects the documented types of a few magic properties along the way:

  • WP_Comment: Adds @phpstan-var numeric-string to $comment_ID, $comment_post_ID, $comment_karma, $comment_parent, and $user_id. ($comment_approved is intentionally excluded since it can also be spam, trash, or post-trashed.)
  • WP_Site: Adds @phpstan-var numeric-string to $blog_id, $site_id, $public, $archived, $mature, $spam, $deleted, and $lang_id.
  • WP_Network: Adds @phpstan-var numeric-string to the private $blog_id property, and documents the corresponding magic $blog_id property (exposed via __get(), which returns (string) $this->get_main_site_id()) with an @property tag and an @phpstan-property numeric-string refinement, as it was previously missing from the class-level tags.
  • WP_Site::$post_count (magic): Corrects the type from int to int|string|false (int|numeric-string|false for PHPStan). The value is lazy-loaded via get_option( 'post_count' ) in WP_Site::get_details(), so despite update_posts_count() storing an integer, it is a numeric string once read back from the database, and false when the option is not set (new sites with no published posts — see Tests_Multisite_Site_Details::test_site_details_cached_including_false_values()). The integer can also be returned within the same request via the options cache and can persist in the site-details object cache.
  • WP_User::$user_status (magic): Adds an @phpstan-property numeric-string refinement. The value comes raw off the wp_users row, where the column always exists.
  • WP_User::$user_level (magic): Corrects the type from int to int|string (int|numeric-string|'' for PHPStan). The value resolves through get_user_meta( ..., 'wp_user_level', true ), which returns a numeric string, or an empty string when the metadata is absent (e.g. a multisite user with no role on the site). It is an integer only after WP_User::update_user_level_from_caps() has assigned one to the instance in the same request, such as via WP_User::set_role() during wp_insert_user(). ($spam/$deleted are left as plain string since on single site they fall through to user meta and return ''.)

WP_Post and WP_Term integer-like fields are not touched because they are actually cast to int during hydration (WP_Post::get_instance()/sanitize_term()).

Existing core call sites are unaffected: nothing in core reads the magic WP_Site::$post_count, and the WP_User::$user_level readers either cast to (int) (wp_set_current_user()) or use loose comparisons in deprecated.php.

Trac tickets: https://core.trac.wordpress.org/ticket/64898, https://core.trac.wordpress.org/ticket/64896

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Fable 5
Used for: Auditing core for remaining DB-backed properties missing numeric-string types, tracing the runtime types of the magic properties, authoring the docblock changes and commit messages, and verifying with PHPCS/PHPStan. All changes were reviewed and directed by me.

---
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.

🤖 Generated with Claude Code

#41 @westonruter
6 weeks ago

In 62640:

Docs: Add numeric-string types to DB-backed class properties.

Add @phpstan-var numeric-string annotations to string properties which hold integer database values:

  • WP_Comment::$comment_ID
  • WP_Comment::$comment_post_ID
  • WP_Comment::$comment_karma
  • WP_Comment::$comment_parent
  • WP_Comment::$user_id
  • the eight WP_Site properties backed by integer columns of the wp_blogs table
  • the private WP_Network::$blog_id property

Previously the numeric-string nature of such properties was only indicated in the property description:

A numeric string, for compatibility reasons.

Additionally, document the magic WP_Network::$blog_id property, which is exposed via __get() as a numeric string but was missing from the class-level @property tags.

Finally, correct the documented types of three magic properties:

  • WP_Site::$post_count is lazy-loaded via get_option(), so it is a numeric string once read back from the database and false when the option is not set (new sites with no published posts); it holds an integer only when served from the options cache in the same request that updated it.
  • WP_User::$user_status always holds a numeric string coming raw off the users table row.
  • WP_User::$user_level resolves through user metadata as a numeric string (or an empty string when the metadata is absent), holding an integer only after WP_User::update_user_level_from_caps() has assigned one to the instance in the same request.

Developed in https://github.com/WordPress/wordpress-develop/pull/12408.
Follow-up to r37657, r37870, r38630, r48941, r62437.

See #44723, #64896, #64898.

@westonruter commented on PR #12407:


6 weeks ago
#42

[!NOTE]
This analysis and comment were authored by AI — Claude Code using the Claude Fable 5 model (claude-fable-5), at @westonruter's direction, per the AI Guidelines.

## PHPStan before/after analysis

Methodology: Full-codebase PHPStan runs on trunk (before) and this branch (after), using the repo's phpstan.neon.dist config overloaded to level 10 with bleedingEdge.neon (the dist config's level 0 barely exercises type resolution, so it would understate the impact of this change). The result cache was fully cleared before each run, since changing a parser-extension class does not reliably invalidate PHPStan's result cache. Errors are matched as a per-occurrence multiset keyed by file + identifier + message (line numbers excluded), so fixed − introduced = net always reconciles.

### Summary

Metric Value
Errors in trunk 34,730
Errors in fix/phpstan-global-docblock-file-scope 34,430
Net change -300
Errors fixed 354
Errors introduced 54
% of baseline fixed 1.02%
Net reduction 0.86%

<sub>Per-occurrence multiset diff: 354 fixed − 54 introduced = 300 net. ✓</sub>

### Errors fixed by identifier (354)

Count Identifier
---:---
148 argument.type
105 property.nonObject
30 offsetAccess.nonOffsetAccessible
29 method.nonObject
18 encapsedStringPart.nonString
16 binaryOp.invalid
6 foreach.nonIterable
1 assign.propertyType
1 echo.nonString

<details><summary>Most-improved files</summary>

Count File
---:---
68 src/wp-admin/edit-form-advanced.php
46 src/wp-admin/edit-form-blocks.php
40 src/wp-admin/edit-form-comment.php
32 src/wp-admin/includes/menu.php
25 src/wp-admin/admin-header.php
24 src/wp-admin/customize.php
23 src/wp-admin/install.php
20 src/wp-admin/post.php
18 src/wp-admin/upgrade.php
13 src/wp-admin/edit.php
11 src/wp-admin/edit-comments.php
8 src/wp-admin/admin.php
6 src/wp-admin/edit-tags.php
5 src/wp-content/themes/twentytwenty/index.php
2 src/wp-admin/admin-footer.php
2 src/wp-admin/menu-header.php
2 src/wp-admin/options-general.php
1 src/wp-admin/includes/export.php
1 src/wp-admin/includes/schema.php
1 src/wp-admin/includes/template.php
1 src/wp-admin/link-parse-opml.php
1 src/wp-content/themes/twentyeleven/content-featured.php
1 src/wp-content/themes/twentyeleven/header.php
1 src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php
1 src/wp-content/themes/twentyten/header.php

</details>

### Errors introduced by identifier (54)

Count Identifier
---:---
14 offsetAccess.invalidOffset
12 argument.type
10 variable.undefined
8 missingType.iterableValue
6 property.nonObject
3 offsetAccess.nonOffsetAccessible
1 method.nonObject

<details><summary>Introduced errors (detail)</summary>

  • src/wp-admin/admin-header.php — [argument.type] Parameter #1 $text of function esc_js expects string, string|null given.
  • src/wp-admin/admin.php — [missingType.iterableValue] PHPDoc tag @var for variable $wp_importers has no value type specified in iterable type array.
  • src/wp-admin/admin.php — [offsetAccess.invalidOffset] (×3) Possibly invalid array key type mixed.
  • src/wp-admin/admin.php — [variable.undefined] (×3) Variable $pagenow might not be defined.
  • src/wp-admin/customize.php — [argument.type] Parameter #1 $required of function is_wp_version_compatible expects string, string|false given.
  • src/wp-admin/customize.php — [argument.type] Parameter #1 $required of function is_php_version_compatible expects string, string|false given.
  • src/wp-admin/edit-comments.php — [argument.type] Parameter #1 $text of function esc_attr expects string, int<min, -1>|int<1, max> given.
  • src/wp-admin/edit-form-advanced.php — [offsetAccess.nonOffsetAccessible] (×2) Cannot access offset string on mixed.
  • src/wp-admin/edit-form-advanced.php — [argument.type] Parameter #1 $text of function esc_attr expects string, int given.
  • src/wp-admin/edit-form-blocks.php — [missingType.iterableValue] PHPDoc tag @var for variable $wp_meta_boxes has no value type specified in iterable type array.
  • src/wp-admin/edit-form-comment.php — [argument.type] Parameter #1 $post of function get_edit_post_link expects int|WP_Post, string given.
  • src/wp-admin/edit-form-comment.php — [argument.type] (×2) Parameter #1 $post of function get_the_title expects int|WP_Post, string given.
  • src/wp-admin/edit.php — [offsetAccess.nonOffsetAccessible] Cannot access offset non-falsy-string on mixed.
  • src/wp-admin/includes/menu.php — [offsetAccess.invalidOffset] (×11) Possibly invalid array key type mixed.
  • src/wp-admin/includes/menu.php — [missingType.iterableValue] PHPDoc tag @var for variable $compat has no value type specified in iterable type array.
  • src/wp-admin/includes/menu.php — [missingType.iterableValue] PHPDoc tag @var for variable $menu has no value type specified in iterable type array.
  • src/wp-admin/includes/menu.php — [missingType.iterableValue] PHPDoc tag @var for variable $submenu has no value type specified in iterable type array.
  • src/wp-admin/includes/menu.php — [argument.type] Parameter #2 $callback of function usort expects callable(mixed, mixed): int, 'sort_menu' given.
  • src/wp-admin/includes/schema.php — [missingType.iterableValue] PHPDoc tag @var for variable $wp_queries has no value type specified in iterable type array.
  • src/wp-admin/install.php — [argument.type] Parameter #1 $version1 of function version_compare expects string, string|null given.
  • src/wp-admin/install.php — [method.nonObject] Cannot call method get_error_message() on string|WP_Error.
  • src/wp-admin/menu-header.php — [missingType.iterableValue] PHPDoc tag @var for variable $menu has no value type specified in iterable type array.
  • src/wp-admin/menu-header.php — [missingType.iterableValue] PHPDoc tag @var for variable $submenu has no value type specified in iterable type array.
  • src/wp-admin/post.php — [property.nonObject] (×3) Cannot access property $post_type on array|WP_Post.
  • src/wp-admin/post.php — [property.nonObject] Cannot access property $post_status on array|WP_Post.
  • src/wp-admin/post.php — [argument.type] Parameter #1 $post of function use_block_editor_for_post expects int|WP_Post, array|WP_Post given.
  • src/wp-admin/post.php — [property.nonObject] (×2) Cannot access property $ID on array|WP_Post.
  • src/wp-admin/upgrade.php — [argument.type] Parameter #1 $version1 of function version_compare expects string, string|null given.
  • src/wp-includes/ms-settings.php — [variable.undefined] (×7) Variable $current_blog might not be defined.

</details>

### Interpretation of the 54 "introduced" errors

None are regressions caused by the visitor change. They fall into three buckets:

  1. Sharper re-statements of pre-existing errors (~22): the same call site now reports a narrower type than mixed, so the diff counts one fixed + one introduced (e.g. post.php: "Cannot access property $post_type on array|WP_Post", previously "on mixed").
  2. Imprecise @global docs now actually consumed (8 × missingType.iterableValue): tags like @global array $menu now feed the analysis, so PHPStan asks for a value type. Fixable by tightening those docblocks.
  3. Latent issues the sharper types exposed (~24): genuinely new findings, e.g. variable.undefined for $pagenow (×3 in admin.php) and $current_blog (×7 in ms-settings.php), plus 14 × offsetAccess.invalidOffset (11 in wp-admin/includes/menu.php) — previously invisible because the variables were mixed.

Net: this change resolves 354 errors and surfaces ~30 previously-invisible latent issues, for a net reduction of 300 errors (−0.86%) at level 10.

🤖 Generated with Claude Code

#43 @westonruter
6 weeks ago

In 62642:

Build/Test Tools: Honor statement-level @global tags in PHPStan.

The GlobalDocBlockVisitor PHPStan extension introduced in [62292] only read @global tags from the enclosing function's docblock and skipped global statements outside any function/method. This meant that file-scope global statements (e.g. in admin templates included into another scope) required a redundant inline @var tag in addition to the @global tag for PHPStan to resolve the variable's type.

Now @global tags in a docblock attached directly to a global statement are honored as well. Statement-level tags take precedence over the enclosing function's tags for the same variable, and handwritten @var annotations continue to take precedence over synthetic ones. At higher PHPStan rule levels this resolves several hundred pre-existing errors, primarily in admin screen templates.

Developed in https://github.com/WordPress/wordpress-develop/pull/12407.
Follow-up to r62292.

See #64898.

#44 @westonruter
6 weeks ago

In 62647:

Docs: Indicate absint() returns non-negative-int for static analysis.

Follow-up to r6222, r29011.

See #64898.

This ticket was mentioned in PR #12426 on WordPress/wordpress-develop by @westonruter.


6 weeks ago
#45

Adds precise, conditional PHPStan return types to the core post retrieval, sanitization, and insertion APIs, along with a small amount of behavior-preserving runtime hardening (and one genuine latent-bug fix) that these types surfaced.

## Conditional return types

Functions whose return type depends on an argument now carry a @phpstan-return keyed on that argument, so static analysis can narrow the result at each call site:

  • get_post(), get_page(), get_page_by_path() — keyed on $output: array<string, mixed>|null for ARRAY_A, array<int, mixed>|null for ARRAY_N, WP_Post|null otherwise.
  • get_children() — keyed first on $args['fields'] ('ids'int[], mirroring get_posts()) and then on $output.
  • get_posts() and WP_Query::query()$args with fields => 'ids'int[], otherwise WP_Post[].
  • wp_get_recent_posts()ARRAY_Aarray<int, array<string, mixed>>, otherwise WP_Post[]|false.
  • get_post_field() / sanitize_post_field() — keyed on $field: int for ID/post_parent/menu_order, non-negative-int[] for ancestors, string otherwise (get_post_field() additionally includes the '' failure return).
  • sanitize_post() — same type in as out (WP_Post, stdClass, or array).
  • get_post_types()'names'string[], otherwise WP_Post_Type[].
  • wp_insert_post(), wp_update_post(), wp_insert_attachment()$wp_error === falseint, otherwise int|WP_Error.

Matching @phpstan-param tags were added where the analyzed return depends on a narrowed input (e.g. $output, $context, $filter).

## Runtime changes

Most of the diff is documentation-only, but a few small code changes were needed for the types to hold:

  • get_post() now null-guards the WP_Post::filter( 'raw' ) result. filter( 'raw' ) calls WP_Post::get_instance(), which can return false for a since-deleted post; the previous code would then call ->to_array() on false. This is a real latent fix, not just a typing change. WP_Post::filter() is retyped WP_Post|false accordingly and its missing summary docblock is filled in.
  • WP_Post::get_instance() cache-hit guard uses instanceof stdClass/instanceof WP_Post (behavior-preserving vs. the old truthiness check) and casts $_post->ID to int before caching.
  • get_post() routes non-numeric scalar $post values straight to null instead of issuing a pointless WHERE ID = 0 query.
  • sanitize_post_field() casts $value to array before array_map( 'absint', ... ) for the ancestors field.
  • WP_Post::get_category() / get_tags() guard against a WP_Error from get_the_terms().
  • Assorted (int) casts on post IDs passed to get_post()/get_instance().

Verified with phpstan-diff against trunk: no new errors on changed lines.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Drafting the conditional return type annotations and the accompanying runtime hardening under my direction. I reviewed, tested, and take responsibility for all of the changes.

@westonruter commented on PR #12426:


6 weeks ago
#46

## PHPStan impact: trunk → this branch

Ran two full PHPStan passes (local phpstan.neon, level 10) on trunk and on this branch to quantify the effect.

Metric Value
------:
Errors in trunk 34,431
Errors in this branch 34,149
Net change −282
Fixed 310
Introduced 28
% of baseline fixed 0.90%
Net reduction 0.82%

Reconciliation checks out: 310 − 28 = 282. A 0.82% net reduction from a mostly-annotation diff — the precise return types let PHPStan resolve mixed/object returns at hundreds of downstream call sites. Top beneficiaries: post.php (−64), class-wp-xmlrpc-server.php (−52), media.php (−15), ajax-actions.php/admin/post.php/class-wp-customize-nav-menus.php (−12 each).

### Errors fixed by identifier (310)

Count Identifier
---:---
110 property.nonObject
77 argument.type
75 offsetAccess.nonOffsetAccessible
13 encapsedStringPart.nonString
9 missingType.iterableValue
8 return.type
7 assign.propertyType
4 offsetAccess.invalidOffset
3 property.notFound
3 method.nonObject
1 binaryOp.invalid

### Triage of the 28 "introduced"

None are regressions in the changed code, with one known-trade-off exception. They fall into four buckets:

1. Latent call-site bugs *exposed* by sharper types (~15). Since get_post() / get_page_by_path() now return array|WP_Post|null precisely (instead of mixed/object), PHPStan can finally see call sites that assume a bare WP_Post:

  • media.php (5) and class-wp-customize-nav-menus.php (7): Cannot access property $ID/$post_title on array|WP_Post|null, expects WP_Post, array|WP_Post|null given.
  • class-wp-query.php (1): passes mixed to get_page_by_path()'s now-typed $post_type.

These are pre-existing fragilities (unchecked null, ignored ARRAY_A case) surfaced by the new types — good candidates for follow-up under this same ticket rather than expanding this PR.

2. Dead-check / unused-type warnings that are artifacts of the narrowed types (3):

  • WP_Post::get_instance()empty( $_post->filter ) flagged because $filter is now the 'raw'|'edit'|… enum (never falsy); it's a defensive check.
  • nav-menu.phpisset() on the non-nullable WP_Post::$ID.
  • _fix_attachment_links()return.unusedType, because precise types made a WP_Error branch provably unreachable.

3. Genuine imprecision in this branch's annotation (2):

post.php — sanitize_post_field() should return array<int<0, max>>|int|string but returns mixed  (×2)

The 'raw' context returns $value unchanged (mixed), but the conditional return declares string for that field class. In practice raw DB values are strings, but PHPStan can't prove it. Leaving this as a deliberate precision-vs-soundness trade-off; the net result is still −282.

4. Nondeterministic churn unrelated to the diff (~7). The WP_Taxonomy (upgrade.php, block-template-utils.php), wp-mail.php, and Twenty Fourteen entries — verified the totals are identical before/after (e.g. WP_Taxonomy-related errors are 260 in both runs), just redistributed between passes. None of that code is touched here.

---

<sub>Analysis performed by Claude (Opus 4.8) via Claude Code, at the request of and reviewed by @westonruter.</sub>

#47 @westonruter
6 weeks ago

In 62648:

Code Quality: Add conditional return types to post functions.

Annotate the core post retrieval, sanitization, and insertion APIs with PHPStan conditional return types keyed on their mode arguments, so static analysis can resolve the concrete result at each call site:

  • get_post(), get_page(), get_page_by_path(), get_children(), and get_post_types() narrow on $output.
  • get_posts() and WP_Query::query() narrow on the fields argument.
  • get_post_field() and sanitize_post_field() narrow on $field.
  • wp_insert_post(), wp_update_post(), and wp_insert_attachment() narrow on $wp_error.

Matching @phpstan-param tags are added where the analyzed return depends on a narrowed input.

Separately, several previously generic or underspecified types are filled in where the precise type is known:

  • WP_Post::to_array() is typed array<string, mixed> rather than a bare array.
  • WP_Post::$filter is annotated with its recognized context values rather than a bare string.
  • WP_Post::filter() is corrected to return WP_Post|false (previously WP_Post), and its missing summary is documented.
  • sanitize_post()'s $post parameter is narrowed from object to stdClass|WP_Post.

Because these functions were previously typed as returning mixed or a bare object, the sharper types let analysis see through to hundreds of downstream call sites, for a net reduction of roughly 280 PHPStan errors across the tree.

A few supporting runtime changes make the types hold:

  • get_post() now guards against the false that WP_Post::filter( 'raw' ) can return for a since-deleted post.
  • WP_Post::get_instance() tightens its cache-hit check.
  • post IDs are cast to int where passed on.

Developed in https://github.com/WordPress/wordpress-develop/pull/12426.
Follow-up to r61789.

See #64898, #64896.

#48 @westonruter
6 weeks ago

In 62651:

Login and Registration: Fix signup and activation URL schemes.

When an already-active signup is revisited, wp-activate.php hard-coded the http:// scheme and passed the scheme-less domain and path through esc_url(), which prepended its own http:// and produced a doubled, broken http://http:// link. Build the full URL with the correct scheme from is_ssl() before escaping it.

Additionally, the signup confirmation heading in wp-signup.php now uses a scheme-relative // URL so the link honors the network's HTTPS configuration instead of forcing http.

This also hardens related code in wp-activate.php and wpmu_activate_signup() against type issues surfaced by PHPStan, including normalizing the signup meta to always be an array.

Developed in https://github.com/WordPress/wordpress-develop/pull/12257.
Follow-up to r12603, r48672, r57625.

Props meet_hasmukh, westonruter.
See #64898.
Fixes #65506.

#49 @westonruter
6 weeks ago

In 62657:

Build/Test Tools: Exclude load-scripts.php/load-styles.php from PHPStan analysis.

These files may emit “Function get_file not found.” errors after a non-dev build. Both of these files use the get_file() function which is located in the already-excluded noop.php.

Props afercia, desrosj, westonruter.
See #64898, #57548.

This ticket was mentioned in PR #12455 on WordPress/wordpress-develop by @westonruter.


6 weeks ago
#50

Adds @phpstan-prefixed generics and conditional return types to the six functions in the slashing/unslashing family, so that static analysis knows a string in yields a string out and an array in yields an array out.

No runtime code changes. This PR only touches docblocks. The plain @param/@return tags that the Code Reference parses are left exactly as they were; all new tags are @phpstan- prefixed and sit below them, per the convention already used elsewhere in core.

## What's wrong today

wp_unslash(), wp_slash(), and stripslashes_deep() all promise "in the same type as supplied", but @return string|array cannot express that. Passing a string gets you back array|string, which then poisons every downstream call:

// src/wp-admin/includes/ajax-actions.php:2324
if ( is_array( $_POST['sidebars'] ) ) {           // caller narrows correctly
        foreach ( wp_unslash( $_POST['sidebars'] ) as $key => $val ) {

PHPStan reports Argument of an invalid type array|string supplied for foreach, only iterables are supported. — because wp_unslash() is declared as possibly returning a string, even though the caller just proved the input is an array.

There is also a pre-existing error on trunk inside wp_slash() itself:

src/wp-includes/formatting.php:5817
Parameter #1 $callback of function array_map expects (callable(mixed): mixed)|null, 'wp_slash' given.

wp_slash() recurses via array_map( 'wp_slash', $value ), which hands each element to wp_slash() as mixed. The narrow @param string|array denies that, so PHPStan rejects the function's own recursion. Note the docblock already says @since 5.5.0 Non-string values are left untouched., so accepting mixed is the documented behavior.

## Two soundness bugs this fixes

An earlier iteration of this branch used a bare @phpstan-return T ("returns its input unchanged"). That is not what these functions do, and it made PHPStan believe two false things:

map_deep( array( '1', '2' ), 'intval' );
// inferred: array{'1', '2'}      actual: array{1, 2}
// map_deep()'s callback is arbitrary and may change the type of every leaf.

wp_unslash( "O\'Brien" );
// inferred: 'O\'Brien'           actual: 'O'Brien'
// String literals must widen — the function rewrites string contents.

Both are fixed by the conditional return types below. stripslashes_from_strings_only() demonstrates why the conditional is required rather than a plain T: with @phpstan-return T, PHPStan correctly rejects the body with should return T but returns string|T of mixed, because T could be a literal-string subtype while stripslashes() returns plain string.

## The changes

Function Return type Rationale
map_deep() arrayarray, objectT, else mixed The callback decides the leaf type. The object branch returns T because map_deep() mutates in place and returns the same instance.
stripslashes_from_strings_only() (T is string ? string : T) Only strings are altered.
stripslashes_deep() string-preserving, array values mapped Its callback only alters strings, so non-strings pass through as T.
wp_slash() same Also fixes the array_map error above.
wp_unslash() same
add_magic_quotes() T of array, array values mapped Lets a precisely-typed superglobal survive wp_magic_quotes().

The shared array-value shape is:

array<key-of<T>, ( value-of<T> is string ? string : value-of<T> )>

### On key-of<T> and value-of<T>

key-of<T> is currently a no-op when T is a template type bound to an array — see phpstan/phpstan#14571 (open, unfixed as of 2.2.5). It is retained because it accurately describes the runtime behavior: map_deep() and friends write back to $value[ $index ], so keys are preserved exactly. It will start paying off when that issue is resolved.

value-of<T> resolves today but yields mixed for superglobal data, because PHPStan models $_GET/$_POST as array<mixed>. PHP guarantees their leaves are always string (?input[post][id]=1 produces string("1"), never int), so this too will sharpen if superglobals ever carry precise types.

## Impact on PHPStan error counts

Measured with level: 10 locally (the committed phpstan.neon.dist runs at level: 0, so CI totals are unaffected). Whole-repo counts:

trunk this branch net
------:---:---:
wp_unslash()/wp_slash() parameter checks 294 0 −294
Everything else 33,180 33,060 −120
Total 33,474 33,060 −414

The −414 headline is misleading and should not be read as "414 bugs fixed." Breaking it down honestly:

1. Genuinely fixed — the narrowing case from the top of this description. foreach.nonIterable on array|string drops from 11 to 9:

if ( is_array( $_POST['sidebars'] ) ) {
        foreach ( wp_unslash( $_POST['sidebars'] ) as $key => $val ) {   // trunk: foreach.nonIterable

2. Silenced, not fixed (294 errors) — adding @phpstan-param T $value overrides the plain @param string|array, so PHPStan stops checking the argument entirely:

// src/wp-admin/admin.php:142
$plugin_page = wp_unslash( $_GET['page'] );
// trunk:  Parameter #1 $value of function wp_unslash expects array|string, mixed given.
// branch: (no error)

These 294 are an artifact of PHPStan typing superglobals as array<mixed>; if $_POST['key'] were string|array<…> it would satisfy @param string|array and they would disappear on their own, with or without this PR.

3. Reworded, still present — a naive multiset diff double-counts these as one fixed and one introduced:

// src/wp-activate.php:23
list( $activate_path ) = explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) );
// trunk:  explode expects string, array|string given.
// branch: explode expects string, mixed given.

4. Newly reported (53 errors, 29 of them Cannot access offset … on mixed) — these flag call sites that do *not* narrow, and are arguably an improvement:

// src/wp-admin/includes/ajax-actions.php:3323
$attachment = wp_unslash( $_POST['attachment'] );   // no is_array() guard → mixed
$id         = (int) $attachment['id'];              // Cannot access offset 'id' on mixed.

So the defensible summary is: a small number of real fixes, 294 checks deliberately traded away, and 53 new errors that mark unnarrowed call sites — not a −414 improvement.

## Verification

  • All six function bodies validate at PHPStan level 10 with no return.type errors.
  • composer lint (PHPCS / WordPress Coding Standards) is clean on both changed files.
  • Inference spot-checks:
Expression trunk this branch
wp_unslash( $string ) array\||string string
wp_unslash( $_POST['k'] ) after is_array() array\||string array<mixed>
wp_slash( array<int, int> ) array\||string array<int>
wp_slash( $dateTimeImmutable ) array\||string DateTimeImmutable
map_deep( ['1','2'], 'intval' ) mixed array<mixed>
wp_unslash( "O\'Brien" ) array\||string string

## Not included

  • esc_sql() / wpdb::_escape() have the same recursive shape and the same @return string|array … in the same type as supplied promise, but that promise is false: _real_escape() returns '' for non-scalars and string otherwise, so esc_sql( array( 1, 2 ) ) returns array( '1', '2' ). It needs a different (string-producing) conditional and a docblock correction. Worth a separate ticket.
  • urlencode_deep(), rawurlencode_deep(), urldecode_deep(), wp_kses_post_deep() are also string-producing rather than string-preserving.
  • The deprecated wp_slash_strings_only(), addslashes_strings_only(), and addslashes_gpc() are exact analogues, left alone intentionally.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Drafting the conditional return types and verifying them against PHPStan (including the map_deep()/wp_unslash() soundness repros), plus computing and decomposing the error-count deltas. Every claim in this description was checked by running PHPStan and PHP directly; the design decisions, the scope of the change, and this final text were reviewed and directed by me.

---
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.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LFXAppq3f99RVSAHaHHMB6

#51 @westonruter
6 weeks ago

In 62672:

Code Quality: Add PHPStan conditional return types to the slashing functions.

Adds @phpstan- prefixed generics and conditional return types to map_deep(), stripslashes_from_strings_only(), stripslashes_deep(), wp_slash(), wp_unslash(), and add_magic_quotes(). Static analysis now knows that a string passed to the slashing functions yields a string, and an array yields an array; this is something the plain @return tags could not express, and which caused wp_unslash() to widen its callers' types to array|string even after an is_array() guard.

The returns are conditional rather than a bare T for two reasons: map_deep()'s callback may change the type of every leaf, and the slashing functions rewrite string contents, so a literal-string type cannot survive.

Developed in https://github.com/WordPress/wordpress-develop/pull/12455.

See #64898.

#52 @westonruter
6 weeks ago

In 62680:

Code Quality: Add conditional return typing for term_exists().

Developed as subset of https://github.com/WordPress/wordpress-develop/pull/12201

Props westonruter, gziolo.
See #64898, #65476.

This ticket was mentioned in PR #12465 on WordPress/wordpress-develop by @westonruter.


6 weeks ago
#53

Adds PHPStan array shapes and narrowed parameter/return types to wp_insert_term(), wp_update_term(), and wp_delete_term() in src/wp-includes/taxonomy.php. This removes the missingType.iterableValue errors reported for each function's $args parameter and return type at PHPStan level 10.

The types describe the contract these functions are *intended* to be called with, rather than what unguarded callers happen to pass today. Notably:

  • wp_update_term() does not accept a query string. Its @param previously documented array|string, but array_merge( $term, $args ) runs before wp_parse_args(), so passing a string is a TypeError. Corrected to array. wp_insert_term() and wp_delete_term() both call wp_parse_args() first, so array|string remains correct for those.
  • parent is typed non-negative-int. sanitize_term_field() puts parent in $int_fields and silently clamps a negative value to 0, reparenting the term to the root. A numeric-string alternative would be redundant: when $args is a query string it matches the string branch of the union, so the array shape only constrains callers passing an actual array, where an int is what should be supplied.
  • wp_delete_term()'s default is typed positive-int, since 0 is discarded by the subsequent term_exists() check.
  • The $args shapes for wp_insert_term() and wp_update_term() are unsealed (...), because $args is forwarded to the pre_insert_term, wp_insert_term_data, wp_insert_term_duplicate_term_check, create_term, wp_update_term_data, and edit_term hooks, where plugins legitimately read arbitrary keys. wp_delete_term()'s shape is sealed, as its $args is consumed locally and never passed to a hook.
  • description is string|null for wp_insert_term() but string for wp_update_term(). This asymmetry is deliberate: wp_insert_term() explicitly coerces a null description (// Coerce null description to strings, to avoid database errors.), while wp_update_term() has no such coercion.
  • wp_delete_term()'s return is narrowed to bool|WP_Error|0, verified against all six return paths. This relies on the term_exists() conditional return type added in r62680.

These annotations surface new argument.type errors at several core call sites that pass $_POST or otherwise unvalidated data directly into these functions (wp-admin/edit-tags.php, wp-admin/includes/ajax-actions.php, wp-includes/nav-menu.php, class-wp-rest-terms-controller.php, class-wp-xmlrpc-server.php). These are pre-existing latent issues that the types now make visible, and are left to be addressed separately.

The two term_id: mixed return errors that remain inside wp_insert_term() and wp_update_term() stem from apply_filters() being typed as returning mixed, and are addressed in a separate pull request.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Reviewing the annotations against the function bodies and every core call site by diffing PHPStan level 10 output before and after; identifying the incorrect array|string type on wp_update_term(), the null description/slug values, the unsealed-shape requirement, and the redundant numeric-string alternatives; and authoring the final non-negative-int/positive-int narrowings. All changes were reviewed, tested, and are owned by me.

---
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.

🤖 Generated with Claude Code

#54 @westonruter
6 weeks ago

In 62682:

Code Quality: Add types for term insert/update/delete functions.

Add PHPStan array shapes for the $args parameter of wp_insert_term(), wp_update_term(), and wp_delete_term(), along with narrowed return types. The types describe the contract these functions are intended to be called with, rather than what unguarded callers happen to pass today.

Developed in https://github.com/WordPress/wordpress-develop/pull/12465.
Follow-up to r62680.

See #64898.

This ticket was mentioned in PR #12485 on WordPress/wordpress-develop by @westonruter.


6 weeks ago
#55

This improves the static analysis types for the post retrieval functions, so that the type of the returned value can be determined from the requested $output.

## Conditional return types

The @phpstan-return types of get_post(), get_page(), get_page_by_path(), get_children(), and wp_get_recent_posts() are narrowed so that the ARRAY_A and ARRAY_N outputs are typed as non-empty-array. This is consistent with WP_Post::to_array(), which always returns at least the object's declared properties and therefore can never return an empty array.

wp_get_post_revision() previously had no conditional return type at all, so its return value was seen as WP_Post|array|null regardless of the requested $output. Describing it precisely resolves 30 pre-existing PHPStan errors in its callers — mostly Cannot access property $ID on array|WP_Post — across wp_restore_post_revision(), wp_delete_post_revision(), wp_xmlrpc_server::wp_restoreRevision(), and wp-admin/revision.php.

Note that non-empty-array is a PHPStan-specific type which phpDocumentor does not understand, so the plain @return tag for WP_Post::to_array() continues to use array<string, mixed>, with the narrower type supplied via @phpstan-return.

## Behavior changes

Two small runtime changes were needed to support the above:

  • trackback_url_list() now bails when get_post() returns null. Previously, calling it with an invalid post ID would fall through to $postdata['post_excerpt'], emitting "Trying to access array offset on value of type null" warnings.
  • wp_get_recent_posts() now collects its ARRAY_A output in a separate $posts variable rather than overwriting the WP_Post objects in $results in place. Reassigning array values over the elements of $results left the variable holding a mix of WP_Post objects and arrays partway through the loop, so static analysis could only ever infer array<WP_Post|array<string, mixed>> for it. Building up a dedicated variable guarantees that everything it contains is an array. The returned value is unchanged.

## A note on list<>

It may be tempting to type these array returns as list<…> rather than array<int, …>, but that would not be sound. The the_posts, posts_results, and posts_pre_query filters allow plugins to return arbitrary arrays — an array_filter() in a the_posts callback is enough to produce a sparse numeric array — and core never re-indexes the result. array<int, …> is therefore the honest type. (get_children() is keyed by post ID in any case.)

## Testing instructions

No behavior change is expected beyond the removal of the PHP warnings noted above. The existing tests pass:

npm run test:php -- tests/phpunit/tests/post.php
npm run test:php -- tests/phpunit/tests/post/revisions.php

Static analysis was verified to introduce no new errors on the changed files (and to fix 30 existing ones).

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Reviewing the change for correctness and consistency, extending it to wp_get_post_revision() and WP_Post::to_array(), verifying with PHPStan/PHPCS/PHPUnit, and drafting the commit message and this description. All changes were reviewed and edited by me.

#56 @westonruter
6 weeks ago

In 62694:

Code Quality: Improve return types for post functions.

Narrow the conditional @phpstan-return types of get_post(), get_page(), get_page_by_path(), get_children(), and wp_get_recent_posts() so that the ARRAY_A and ARRAY_N outputs are typed as non-empty-array instead of array. This is consistent with WP_Post::to_array(), which always returns at least the object's declared properties and so can never yield an empty array. Slight refactoring is done on wp_get_recent_posts() to support static analysis.

Additionally, wp_get_post_revision() had no conditional return type at all, so its result was seen as WP_Post|array|null regardless of the requested $output. Describing it precisely resolves 30 pre-existing Cannot access property $ID on array|WP_Post errors in its callers.

Furthermore, trackback_url_list() now bails when get_post() returns null. Previously it fell through to $postdata['post_excerpt'], emitting "Trying to access array offset on value of type null" warnings when called with an invalid post ID.

Developed in https://github.com/WordPress/wordpress-develop/pull/12485.
Follow-up to r62648.

See #64898.

This ticket was mentioned in PR #12488 on WordPress/wordpress-develop by @westonruter.


5 weeks ago
#57

Teaches static analysis that certain functions never come back, so it knows the code following a call to them is unreachable — and, where returning depends on a parameter, describes each case exactly instead of over-widening to the union of both.

Native never is PHP 8.1+, so all type work here is PHPDoc-only. Aside from two bug fixes noted at the end, there are no behavioral changes.

wp_die() itself is already typed ( $args is array{exit: false} ? void : never ) on trunk. This branch extends that idea outward to the functions built on top of it, then uses the result.

## 1. @return never on functions that always terminate (30)

The core primitives first, since almost everything else depends on them: wp_send_json(), wp_send_json_success(), wp_send_json_error(), wp_nonce_ays(), do_favicon(), and WP_Ajax_Response::send().

Then the rest: six WP_Customize_Manager methods (wp_die(), save(), refresh_nonces() and three request handlers), three WP_Customize_Nav_Menus Ajax methods, WP_Customize_Widgets::wp_ajax_update_widget(), two Custom_Background and three Custom_Image_Header Ajax methods, WP_List_Table::ajax_response(), comment_footer_die(), redirect_post(), install_theme_information(), WP_Plugin_Dependencies::check_plugin_dependencies_during_ajax(), graceful_fail(), wpmu_admin_do_redirect(), ms_not_installed(), and WP_Sitemaps_Stylesheet::render_stylesheet().

Candidates were found by walking the AST of every non-third-party file under src/ and keeping only declarations that contain no return statement in their own scope *and* terminate on every branch. Each survivor was then independently confirmed by PHPStan, which reports return.never when a @return never is wrong — both for an explicit return and for falling off the end.

Deliberately excluded:

  • The wp_ajax_* handlers in ajax-actions.php. They do always terminate, but nothing calls them directly, so the annotation would buy no call-site narrowing.
  • IXR_Server::error()/output()/serve(). IXR is vendored third-party code, and wp_xmlrpc_server extends IXR_Server while overriding error() with a method that *can* return. Since never is a bottom type, annotating the parent would break return-type covariance.
  • WP_List_Table::get_columns()/prepare_items()/ajax_user_can() and WP_Widget::widget(). These die() with a "must be overridden in a subclass" message, but they are abstract by convention — subclasses return real values.

## 2. Conditional return types (13)

The seven wp_die() handlers_default_wp_die_handler(), _ajax_wp_die_handler(), _json_wp_die_handler(), _jsonp_wp_die_handler(), _xmlrpc_wp_die_handler(), _xml_wp_die_handler() and _scalar_wp_die_handler() — each gain the same conditional wp_die() already carries:

@phpstan-return ( $args is array{exit: false} ? void : never )

Six more functions terminate on one path and return on another, with a parameter deciding which:

Function Conditional type Why
trackback_response() ($error is 1\||true ? never : void) Dies after emitting the error response when $error is truthy.
wp_protect_special_option() ($option is 'alloptions'\||'notoptions' ? never : void) Dies for the protected option names.
redirect_canonical() ($do_redirect is true ? null : string\||null) With $do_redirect true it either exits or returns null; the string is unreachable. Only the recursive redirect_canonical( $url, false ) call can produce one.
check_admin_referer() ($action is -1 ? int\||false : int) false is only reachable in the deprecated no-action case. Callers passing a real action get an int or do not come back at all.
check_ajax_referer() ($stop is true ? int : int\||false) Same, keyed on $stop, which defaults to true.
get_cli_args() ($required is true ? string\||true : string\||true\||null) With $required true the function exits rather than returning null.

redirect_canonical()'s @return already *said in prose* "Never returns if a redirect occurs, depending on $do_redirect" — this just encodes it.

never is claimed only for parameter values where termination is certain. Over-declaring void where the function actually dies merely loses precision at the call site; claiming never where the function can return would be unsound and would mark live code unreachable.

get_cli_args() also declared @return string|true|null|never. never is the bottom type, so in a union it is absorbed and said nothing; the exit is now described in prose instead.

## 3. wp_die() PHPDoc

$message has always accepted an int — the legacy Ajax protocol responds with -1 (not permitted), 0 (failure) and 1 (success), and check_ajax_referer() dies with -1 — but the type was documented as string|WP_Error. Now string|WP_Error|int, with the @param block realigned.

The PHPStan range is int<-1, max> rather than int<-1, 1>: core also calls wp_die( time() ) in six places in ajax-actions.php, sending a Unix timestamp as the response body. -1 is the only negative value passed anywhere in core.

## 4. Short-circuits in ajax-actions.php

With wp_die() and wp_send_json_error() typed as terminating, PHPStan can see where get_post() may return null and the result is dereferenced anyway. Four handlers gained a guard:

  • wp_ajax_add_meta() — folded ! $post into the existing capability check. $post is only dereferenced in the *add* branch ($post->post_status, $post->post_type); the *update* branch finds the row by meta_id and never touches it, so guarding earlier would have broken a working path.
  • wp_ajax_inline_save()wp_die() before $post['post_content'].
  • wp_ajax_save_attachment() and wp_ajax_save_attachment_compat()wp_send_json_error() before $post['post_type'], matching the failure idiom immediately below in each.

All four sit behind current_user_can( 'edit_post', … ), which already returns false for a nonexistent post (map_meta_cap() maps it to do_not_allow), so these are unreachable in practice today. They are defensive narrowing that also covers the delete-between-check-and-fetch race. This is what unlocks the 190 fixed errors in this file.

## 5. Two bug fixes

src/wp-admin/network.php built its "You must define the WP_ALLOW_MULTISITE constant" notice with printf() instead of sprintf(). printf() writes to the output buffer immediately and returns a byte count, so the message was emitted bare — ahead of wp_die()'s own markup — and wp_die() then rendered the integer byte count as its message body. Every other wp_die() call in core uses sprintf(); this was the only outlier. Surfaced by PHPStan once wp_die() had a precise $message type.

wp_ajax_save_attachment()/_compat() and friends — see §4; the missing null guards were real, if currently unreachable.

## PHPStan impact

Both passes run on a cold result cache. This matters: a warm cache can serve a stale result for a file whose PHPDoc the branch edited and silently drop findings on exactly the code under review, which is a correctness problem rather than merely a staleness one.

Metric Value
Errors on trunk 33,128
Errors on this branch 32,859
Net change −269
Errors fixed 308
Errors introduced 39
Reconciliation ✓ (308 − 39 = 269)

Fixed, by identifier: 211 argument.type, 37 missingType.return, 20 property.nonObject, 15 offsetAccess.nonOffsetAccessible, 8 offsetAccess.notFound, 4 binaryOp.invalid, 4 variable.undefined, 4 offsetAccess.invalidOffset, 3 foreach.nonIterable, 1 missingType.iterableValue, 1 assign.propertyType.

Most-improved files: ajax-actions.php (190), privacy-tools.php (39), functions.php (12), class-wp-customize-nav-menus.php (11), then comment.php, class-wp-customize-manager.php and class-wp-customize-selective-refresh.php at 10 each.

### The 39 "introduced" errors are not 39 regressions

  • ~15 are the same errors re-worded. On trunk, src/wp-admin/comment.php:304 reads *"Cannot access property $comment_post_ID on array|WP_Comment|null"*; on this branch it reads *"…on array|WP_Comment"*. Because wp_die() after if ( ! $comment ) is now never, the null branch is correctly pruned — the type gets strictly better. A diff keyed on message text counts an improved message as one fixed plus one introduced. The underlying array|WP_Comment complaint is pre-existing, from get_comment( $id, ARRAY_A ).
  • 10 are genuinely unreachable code, correctly identified:
src/wp-admin/includes/ajax-actions.php:          98, 2711, 2856
src/wp-admin/post.php:                           128, 248
src/wp-includes/class-wp-customize-manager.php:  1939, 3169, 3200
src/wp-includes/pluggable.php:                   1396
src/wp-includes/sitemaps/class-wp-sitemaps.php:  187

For example ajax-actions.php:98 is $wp_list_table->ajax_response(); wp_die( 0 );ajax_response() already ends in wp_die(), so the trailing wp_die( 0 ) can never run. Likewise post.php:128 is redirect_post( $post_id ); exit;. These are harmless belt-and-braces terminators rather than bugs, so they are left alone here and can be cleaned up separately.

  • 3 are in canonical.php — the nested lowercase_octets() helper inside redirect_canonical(), now reachable for analysis.
  • The remainder is latent looseness at wp_die() call sites (mixed, string|false, string|null passed as $message) that the newly-precise signature makes visible.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Substantially authored by AI. The AI wrote an AST-based analyzer to find every function under src/ whose control-flow paths all terminate, applied the annotations, and verified each against PHPStan — including negative controls proving PHPStan rejects a wrong @return never, and PHPStan\dumpType probes proving each conditional type resolves as intended. It also found the network.php printf()/sprintf() bug. I have reviewed and take responsibility for all of it; the exclusion decisions (Ajax handlers, IXR, the "must be overridden" stubs) and the int<-1, max> range were settled in review.

#58 @westonruter
5 weeks ago

In 62703:

Code Quality: Document int as valid message type for wp_die() and fix erroneous printf() arg.

In addition to wp_die() taking a string|WP_Error message, there are also ajax handlers that pass -1, 0, 1, and time(). Refining this type for the $message param exposed an erroneous printf() being passed into wp_die() when sprintf() was intended.

Developed in https://github.com/WordPress/wordpress-develop/pull/12488.
Follow-up to r62177, r34292.

See #64898.

#59 @westonruter
5 weeks ago

In 62704:

Docs: Add never return types to functions that always terminate.

Documents @return never on 30 functions and methods whose every control-flow path ends in exit, die(), wp_die(), or wp_send_json_*(), so static analysis knows the code following a call to them is unreachable. Native never requires PHP 8.1, so these are PHPDoc-only.

Where terminating depends on a parameter, a conditional @phpstan-return describes each case rather than over-widening to the union of both. The seven wp_die() handlers gain the same conditional wp_die() itself already carries. trackback_response() and wp_protect_special_option() never return for the parameter values that die, while redirect_canonical(), check_admin_referer(), check_ajax_referer(), and get_cli_args() narrow their return types accordingly.

The bottom type never was also incorrectly declared in the return type union for get_cli_args(), so it was removed.

Developed as subset of https://github.com/WordPress/wordpress-develop/pull/12488.
Follow-up to r62703, r62178, r62177.

Props westonruter, mukesh27.
See #64896, #64898.

#60 @westonruter
5 weeks ago

In 62705:

Code Quality: Short-circuit Ajax handlers when the supplied post does not exist.

get_post() returns null for a nonexistent post, but four Ajax handlers dereferenced the result without checking: wp_ajax_add_meta(), wp_ajax_inline_save(), wp_ajax_save_attachment(), and wp_ajax_save_attachment_compat(). Each now bails when null is returned. Since these functions return never, subsequent references to $post do not cause static analysis errors. In wp_ajax_add_meta() the check is folded into the existing capability check, since $post is only dereferenced in the branch that adds meta.

These paths are unreachable in core, as current_user_can( 'edit_post', ... ) already blocks for a nonexistent post. The guards are defensive as a plugin filtering capabilities could still reach them, and they give static analysis the non-null narrowing it needs.

Developed as subset of https://github.com/WordPress/wordpress-develop/pull/12488.
Follow-up to r62704, r62703.

See #64898.

This ticket was mentioned in PR #12491 on WordPress/wordpress-develop by @westonruter.


5 weeks ago
#61

Improves PHPStan static-analysis coverage for WP_Post, tightening the types of its array form and properties to what the code actually guarantees.

## Changes

  • WP_Post::to_array() — adds a Data_Array @phpstan-type shape describing every key it returns, used as the method's @phpstan-return. The shape is open (...) because WP_Post is #[AllowDynamicProperties] and instances routinely carry extra columns, so sealing it would produce false "offset does not exist" errors.
  • Refined value types, only where the value set is genuinely closed:
    • ID, post_parentnon-negative-int (never negative; 0 is valid for unsaved objects, so deliberately not positive-int).
    • post_author, comment_countnumeric-string.
    • post_date(_gmt), post_modified(_gmt)non-empty-string (NOT NULL DATETIME columns).
    • The always-present magic keys ancestors, page_template, post_category, tags_inputWP_Post::__isset() returns true for all four, so to_array() always appends them.
    • Fields whose values are extensible by plugins (post_status, post_type, comment_status, ping_status) are intentionally left as string.
  • WP_Post::$filter — corrected to string|null / the six sanitize contexts plus 'sample' plus null. It had been mistyped (as string, then as the six contexts), both of which wrongly excluded null (a WP_Post built from a raw row has no filter until sanitized, and core relies on this via isset()/empty() checks) and 'sample' (assigned by get_sample_permalink() since [8526] / WP 2.7.1 to keep the mutated object out of the cache). sanitize_post_field()'s $context is widened to accept 'sample' accordingly — it already handles it at runtime, falling through to the display branch.
  • get_post_ancestors()list<non-negative-int> return type.
  • trackback_url_list() — fetches the WP_Post and calls to_array() so the accessed keys are known, guarding against a null post.
  • WP_Customize_Nav_Menu_Item_Setting — casts the int from get_current_user_id() to string to honor the numeric-string post_author.

## PHPStan impact (level 10)

Net −8 errors (24 fixed, 16 "introduced").

The 16 "introduced" are not regressions. They are pre-existing latent errors at distant call sites (e.g. esc_attr( $post->ID ), isset( $post->ID )) that PHPStan now prints with the sharper int<0, max> type instead of int — each has a byte-identical counterpart on trunk at the same line, so a message-based diff counts one re-worded error as one fixed + one introduced. None of those sites is in a file this PR edits.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Static-analysis-driven iteration and drafting the PHPDoc/type annotations. Every change was reviewed, verified against PHPStan (level 10) and PHPCS, and edited by me; I take responsibility for the result.

This ticket was mentioned in PR #12497 on WordPress/wordpress-develop by @Soean.


5 weeks ago
#62

Both wp_send_json_error() and WP_Customize_Manager::wp_die() are documented with @return never and always terminate execution, so a subsequent return statement is never reached.

This also makes the class more consistent, as other wp_send_json_error() calls in the same class, e.g. for invalid_nonce and changeset_locked in handle_changeset_trash_request(), are already not followed by a return statement.

Follow-up to [62704]

Trac ticket: https://core.trac.wordpress.org/ticket/64898

@westonruter commented on PR #12491:


5 weeks ago
#64

👉🏻 In a follow-up ticket, there are fixes needed for get_default_post_to_edit(). It is erroneously setting a read-only post_category, it is setting post_author to an empty string when it could be set to '0', and it is setting post_date and post_date_gmt to empty strings when they should be '0000-00-00 00:00:00'. This would allow undoing some of d10c9d4b3938d2440446c9a2a2d0047d7a7791d4 to re-narrow those types.

#65 @westonruter
5 weeks ago

In 62717:

Code Quality: Improve WP_Post type coverage.

Add a Data_Array array-shape type describing every key returned by WP_Post::to_array(), and tighten each WP_Post property to the narrowest type the schema and code guarantee:

  • ID and post_parent become non-negative-int.
  • comment_count becomes numeric-string, and post_author becomes numeric-string|'' (a user ID, or an empty string for a default post that has not yet been assigned an author).
  • The always-populated post_status, comment_status, ping_status, and post_type slugs become non-empty-string, left open rather than enumerated so custom statuses and types remain valid.
  • The magic ancestors, post_category, and tags_input accessors become precise lists (list<non-negative-int> and list<non-empty-string>).

The shape stays open because WP_Post permits dynamic properties. Fields that can legitimately be empty, including the datetime fields (which get_default_post_to_edit() may leave empty), stay string, and menu_order stays int since it can be negative.

Correct WP_Post::$filter, which was previously typed without null or the 'sample' context: an unsanitized post has no filter (checked in core via isset() and empty()), and get_sample_permalink() has assigned 'sample' since [8526]. Widen sanitize_post_field()'s $context to accept 'sample' to match, which it already treats as a 'display' context.

Also type get_post_ancestors() as returning list<non-negative-int>, read the post in trackback_url_list() via to_array() while guarding against a missing post, and cast post_author to a string in WP_Customize_Nav_Menu_Item_Setting and inject_ignored_hooked_blocks_metadata_attributes(), which each populate an object before passing it to new WP_Post().

Developed in https://github.com/WordPress/wordpress-develop/pull/12491.
Follow-up to r62648, r62694.

See #64898, #64896.

#66 @westonruter
5 weeks ago

In 62719:

Code Modernization: Add a polyfill for PHP 8.6's clamp().

Introduce a clamp() polyfill in compat.php so that core, plugins, and themes can call the function on every supported PHP version without a version check. It returns the value when it lies within the bounds and the nearest bound otherwise, and throws a ValueError when $min is greater than $max or when either bound is NAN. On PHP 7.x, an InvalidArgumentException is thrown instead, since the ValueError class does not yet exist.

Adopt the function at the two places in core that were clamping via a nested min( max( … ) ): the oEmbed response width in get_oembed_response_data(), and the minimum font size factor in wp_get_typography_font_size_value().

Additionally, fix PHPStan errors in code that touches the newly-introduced clamp() usage. Add types to wp_get_typography_value_and_unit() and refactor the parsing to allow static analysis to identify the shape of $matches and avoid issues with passing special regex characters in units.

Developed in https://github.com/WordPress/wordpress-develop/pull/11669.
Follow-up to r35436, r54260, r55947.

Props soean, mukesh27, westonruter.
See #64898.
Fixes #65143.

This ticket was mentioned in PR #12441 on WordPress/wordpress-develop by @johnbillion.


5 weeks ago
#67

This adds a Hook_Callback PHPStan type to represent the shape of a callback used internally in WP_Hook and used in return values of array access methods.

Also narrows a few other documented types.

#68 @johnbillion
5 weeks ago

In 62733:

Code Quality: Tighten the documented types in WP_Hook.

This adds a Hook_Callback PHPStan type to represent the shape of a callback used internally in WP_Hook and used in return values of array access methods.

Developed in: https://github.com/WordPress/wordpress-develop/pull/12441

Props westonruter

See #64898

#70 @westonruter
5 weeks ago

In 62752:

General: Allow configuration of speculative loading defaults via env variables and constants.

Introduce two overrides that let a site or hosting provider change the default speculative loading configuration that the auto value resolves to, without having to ship an mu-plugin:

  • WP_SPECULATIVE_LOADING_DEFAULT_MODE (prefetch or prerender)
  • WP_SPECULATIVE_LOADING_DEFAULT_EAGERNESS (conservative, moderate, or eager).

Each may be supplied as an environment variable, read via getenv(), or as a constant of the same name that takes precedence over it, mirroring how wp_get_environment_type() resolves WP_ENVIRONMENT_TYPE. An unrecognized value falls back to the core default.

These overrides only change what auto resolves to, so an explicit mode or eagerness supplied through the wp_speculation_rules_configuration filter still wins. An eagerness of immediate is rejected because WordPress does not permit it for the document-level rules it generates; accepting it would cause WP_Speculation_Rules::add_rule() to reject the rule and leave the page with no speculation rules at all.

Also relax WP_Speculation_Rules::is_valid_mode() and WP_Speculation_Rules::is_valid_eagerness() to accept mixed, so an arbitrary value from the filter is validated and rejected rather than raising a TypeError.

Fix PHPStan errors in speculative loading functions resulting from insufficient typing.

Developed in https://github.com/WordPress/wordpress-develop/pull/12514.
Follow-up to r59837.

Props westonruter, mukesh27, adamsilverstein, swissspidy.
See #64066, #62503, #64896, #64898.
Fixes #65624.

#71 @westonruter
5 weeks ago

In 62763:

Customize: Remove unreachable return statements in WP_Customize_Manager.

This addresses 3 PHPStan errors:

Unreachable statement - code above always terminates.

Each line of code is unreachable because the preceding statement exits execution.

Developed in https://github.com/WordPress/wordpress-develop/pull/12497.
Follow-up to r62704.

Props soean, mukesh27.
See #64898.

This ticket was mentioned in PR #12588 on WordPress/wordpress-develop by @westonruter.


5 weeks ago
#72

The wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list() functions are key utility functions. The wp_parse_id_list() function was used in a security fix for 7.0.2 in r62771 (97b5a75246f6c82fe9d9f0ba492f06d93b602b98). However, these functions can be further hardened to give precise types and to address various PHPStan errors at rule level 10:

wp_parse_list() (5027–5029)

  • missingType.iterableValue ×2 — $input_list param and return are bare array.
  • return.typepreg_split() can return false, which isn't in the declared array return.

wp_parse_id_list() (5047)

  • missingType.iterableValue — bare array param.

wp_parse_slug_list() (5062, 5065)

  • missingType.iterableValue — bare array param.
  • argument.typearray_map( 'sanitize_title', ... ): sanitize_title()'s first param is string, but the array's value type is mixed, so it isn't accepted as callable(mixed): mixed. (absint in wp_parse_id_list doesn't trip this because its param is mixed.)

Furthermore, these functions do not consistently return lists. The use of array_filter() can cause sparse arrays to be returned unexpectedly, and also for passed associative to have their keys passed through. (⚠️ Maybe this is done intentionally???)

Trac ticket: https://core.trac.wordpress.org/ticket/64898

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Opus 4.8
Used for: Coming up with some test assertion logic. Obtaining PHPStan errors.

#73 @westonruter
4 weeks ago

In 62797:

Code Quality: Improve typing for wp_parse_list() et al.

This adds precise @param and @return typing for the wp_parse_list(), wp_parse_id_list(), and wp_parse_slug_list() functions; native array return types are also added. It also adds casting and type guarding to guarantee the types of the values involved. Descriptions are updated to indicate that lists may not be returned, which may be unexpected given the function names; instead, sparse arrays or even associative arrays may be returned. Additionally, typically invalid ID values like zero may be in the array returned by wp_parse_id_list() and an empty string may be in the array returned by wp_parse_slug_list().

Tests are added to ensure existing behavior is preserved. This fixes 6 PHPStan errors at rule level 10.

Developed in https://github.com/WordPress/wordpress-develop/pull/12588.
Follow-up to r38832, r44546, r57737, r62647, r62771.

See #64898.

This ticket was mentioned in PR #12597 on WordPress/wordpress-develop by @westonruter.


4 weeks ago
#74

Updates two PHPStan Composer dependencies used for static analysis:

  • phpstan/phpstan: 2.2.22.2.5
  • phpstan/phpstan-phpunit: 2.0.162.0.18

### phpstan/phpstan (2.2.2 → 2.2.5)

2.2.3

  • Improvements: result cache now invalidates only package-dependent files on a Composer package change; type predicates supported on callable/Closure types; several regex/string-narrowing improvements (e.g. ::class in array shape keys, ob_get_contents() et al. narrowed to non-false during output buffering).
  • Bugfixes: 52 issues fixed, including several BooleanAnd/BooleanOr conditional-expression-holder edge cases, match subject narrowing, and preg_match_all() return type inference.
  • Performance: multiple hot-path optimizations in the type system and scope resolution, plus parallel analysis job striping improvements.

2.2.4 — "RAMpocalypse": substantially reduced memory usage (PHPStan analysing itself dropped from 2.8 GB to 2.1 GB total worker memory), plus supporting bugfixes for boolean conditional holders and output-buffer tracking.

2.2.5

  • Improvements: updated nikic/PHP-Parser to 5.8.0; narrows explode() when the delimiter is a known substring.
  • Bugfixes: allows null values in the native preg_replace_callback callback array type when PREG_UNMATCHED_AS_NULL is used.
  • Performance: further caching/LRU-capping work (member cache, resolved type aliases, cached parser sources) and PHP 8.5 partitioned cookie attribute support.

Full details: https://github.com/phpstan/phpstan/releases

### phpstan/phpstan-phpunit (2.0.16 → 2.0.18)

2.0.17 — Adds ClassAttributeRequiresPhpVersionRule and sanity/range checking for #[RequiresPhp]/#[RequiresPhpunit] attribute values (the rest of the changes are CI/tooling maintenance).

2.0.18 — Fixes #[RequiresPhpunit] version requirements being evaluated against the PHP version instead of the PHPUnit version, and adds default values for the bool parameters of AttributeVersionRequirementHelper.

Full details: https://github.com/phpstan/phpstan-phpunit/releases

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Sonnet 5
Used for: Drafting this pull request description (summarizing upstream changelogs); the dependency version bumps themselves were made directly by me.

@westonruter commented on PR #12597:


4 weeks ago
#75

Note: These two dependencies were updated 2 weeks ago.

#76 @westonruter
4 weeks ago

In 62798:

Build/Test Tools: Update phpstan/phpstan to 2.2.5 and phpstan/phpstan-phpunit to 2.0.18.

Developed in https://github.com/WordPress/wordpress-develop/pull/12597.
Follow-up to r62495, r62618,

See #64898.

This ticket was mentioned in PR #12606 on WordPress/wordpress-develop by @westonruter.


4 weeks ago
#77

Improves type specificity across the comment APIs, continuing the work done for posts in r62437, r62648, r62694], and r62717.

## WP_Comment

  • Adds a Data_Array array shape describing the keys returned by WP_Comment::to_array(), and narrows the properties it covers: comment_approved becomes non-empty-string, and the values core uses for it and for comment_type are documented. comment_type is deliberately not narrowed, because comments created before 5.5.0 may store an empty string rather than comment, which is why get_comment_type() normalizes that case on read.
  • Declares the 21 post fields that WP_Comment::__get() proxies to the comment's post as @property-read, typed to match the corresponding WP_Post property. These were previously invisible to static analysis, IDE completion, and the generated documentation.
  • Corrects $children, which is null until get_children() populates it, and replaces the array<int|numeric-string, WP_Comment> key type used across these APIs with array<int, WP_Comment>. A numeric-string array key cannot exist in PHP, since numeric string keys are coerced to integers on assignment.

## WP_Comment::get_children()

  • Adds conditional return types for the count, fields, and format modes, and documents every argument core or the plugin ecosystem actually passes: fields, count, type, number, post_id, and order. Several were already in use but undocumented.
  • A count or fields query now returns its result directly rather than storing it in the children cache. That cache holds WP_Comment objects and is read back by add_child(), get_child(), and the flat format, so writing an integer or a list of IDs into it left the object returning the wrong thing on a subsequent call. This resolves a TODO left in the method.

One commit in this branch, "Add native return types", added an array return type to get_children(), which made the method fatal when asked for a count. WP_REST_Comments_Controller::prepare_links() calls it with count => true, so this raised TypeError: WP_Comment::get_children(): Return value must be of type array, int returned and failed 102 of the REST comment tests. It is reverted later in the branch, but is left in the history rather than rebased away, since the fix is easier to follow with the mistake visible.

## WP_Comment_Query and get_comments()

  • Adds conditional return types to get_comments() and get_approved_comments(), keyed on count and fields. get_approved_comments() is keyed on $post_id first, since it returns an empty array when that is falsey, before $args is parsed at all.
  • Narrows comment ID arrays to non-negative-int[], and types WP_Comment_Query::$comments as null until a query is run. WP_Comment_Query::__construct() only runs a query when given one, so a bare new WP_Comment_Query() leaves the property unset.

## Known gaps

Eight PHPStan errors remain inside WP_Comment_Query::get_comments(), from two causes: (int) and intval() do not express what the unsigned schema guarantees, and the comment ID cache is read back as mixed. Both are fixable together in a follow-up. Switching the casts to absint() resolves the first half, but is a runtime change and is out of scope here.

The comments_pre_query filter can return an arbitrarily shaped array that is assigned straight to $comments, so the declared type for that property describes intent rather than a guarantee.

## Testing

phpstan-diff is clean on the changed lines, PHPCS reports nothing new, and the comment, note, and REST comment suites pass locally (540, 78, and 200 tests respectively). The full suite has not been run locally and is left to CI.

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: A review of the already-committed typing work, which surfaced most of the issues fixed here, and drafting of the annotations, docblocks, and commit messages. Every change was reviewed and frequently rewritten by me, and several of the better solutions are mine rather than the model's, including splitting the get_children() guard into separate branches so each narrows independently, and asserting count as present-and-false rather than absent so the conditional return type resolves. The model also asserted at one point that core's REST controller passed only documented arguments, which was wrong and which I caught; verifying that claim is what uncovered the TypeError described above.

This ticket was mentioned in PR #12618 on WordPress/wordpress-develop by @westonruter.


4 weeks ago
#78

Trac ticket: https://core.trac.wordpress.org/ticket/64898

Fixes:

  • Missing types
  • Erroneous fallback case for MockAction::current_filter() in how it gets the last key of $wp_actions.
  • Fix MockAction::get_call_count() to handle getting counts of a provided filter.

## Use of AI Tools

None

#79 @westonruter
4 weeks ago

In 62808:

Build/Test Tools: Fix typing and logic issues in MockAction.

  • Add type information.
  • Fix erroneous fallback case for MockAction::current_filter() in how it gets the last key of $wp_actions.
  • Fix MockAction::get_call_count() to handle getting counts of a provided filter in addition to a provided action.

Developed in https://github.com/WordPress/wordpress-develop/pull/12618.

See #64898, #64894.

#80 @westonruter
4 weeks ago

In 62813:

Code Quality: Ensure wp_filesize() always returns a non-negative-int.

A negative file size is clearly impossible, and the return value of 0 is already documented as being the error case.

  • Values filtered by pre_wp_filesize and wp_filesize are cast to int if they are numeric.
  • Non-numeric values returned by the wp_filesize filter are discarded in favor of zero.
  • Negative values filtered by the pre_wp_filesize filter are treated the same as null (and do not short-circuit).
  • Negative values returned by the wp_filesize filter are clamped to be at least zero.

Developed as part of https://github.com/WordPress/wordpress-develop/pull/12611.
Follow-up to r52837, r52932.

Props westonruter, apermo.
See #65670, #64898.

#81 @westonruter
4 weeks ago

In 62822:

Code Quality: Improve comment API type coverage.

Add a Data_Array array-shape type describing the keys returned by WP_Comment::to_array(), and narrow the properties it covers: comment_approved and the two datetime fields become non-empty-string, and the values core uses for it and for comment_type are documented. comment_type itself stays a plain string, because comments created before 5.5.0 may store an empty string rather than 'comment', which is why get_comment_type() normalizes that case on read.

Declare the 21 post fields that WP_Comment::__get() proxies to the comment's post as @property-read, typed to match the corresponding WP_Post property. These were previously invisible to static analysis, IDE completion, and the generated documentation. Also correct WP_Comment::$children, previously a bare array, which is null until get_children() populates it, and type the comment arrays keyed by comment ID as array<int, WP_Comment>, since PHP coerces the numeric string comment_ID to an integer key on assignment.

Add conditional return types to WP_Comment::get_children(), get_comment(), get_comments(), and get_approved_comments(), and document every argument ::get_children() actually accepts, several of which were already in use but undocumented. Comment ID arrays are narrowed to non-negative-int[], and WP_Comment_Query::$comments is typed as null until a query is run.

Several latent issues surfaced by the analysis are fixed:

  • WP_Comment::get_children() now returns a count or fields query directly rather than storing it in the children cache. That cache holds WP_Comment objects and is read back by add_child(), get_child(), and the 'flat' format, so writing an integer or a list of IDs into it left the object returning the wrong thing on a subsequent call.
  • get_comment() now hands only numeric values to WP_Comment::get_instance(). Previously anything that was not a WP_Comment or some other object fell through to be cast to an integer ID, even if it wasn't numeric. Now null is returned in such cases.
  • WP_Comment::get_instance() ignores a non-object read from the comment cache rather than passing it to the WP_Comment constructor, where get_object_vars() would raise a TypeError.
  • WP_Comment::__isset() returns false, and WP_Comment::__get() returns null, when the comment's post no longer exists, instead of raising a TypeError and a warning respectively. __get() also returns null when the comment is not attached to a post at all; previously get_post( 0 ) fell back to the global $post, so the getter returned an unrelated post's field even though __isset() reported that same property as unset.

Developed in https://github.com/WordPress/wordpress-develop/pull/12606.
Follow-up to r34583, r62648, r62694, r62717.

Props westonruter, adamsilverstein.
See #64898.

#82 @westonruter
4 weeks ago

In 62835:

Code Quality: Preserve string[] input type in wp_parse_list() return.

This prevents unintentional widening of a string[] input to a scalar[] output, since strings are scalars.

Follow-up to r62797.

See #64898.

#83 @westonruter
4 weeks ago

In 62836:

Code Quality: Document that sanitize_key() returns lowercase-string.

This is a narrower PHPStan type compared to just string.

See #64898.

#84 @SergeyBiryukov
4 weeks ago

In 62842:

Plugins: Remove redundant type casting in wp_filter_build_unique_id().

The (object) type casting was preceded by an is_object() check and can be safely removed.

The isset() language construct is enough to check for an array when detecting malformed callbacks, so the (array) type casting is not required.

Removing the type casting results in an additional performance improvement up to ~8% for the function.

Follow-up to [62408].

See #58291, #64898.

This ticket was mentioned in PR #12725 on WordPress/wordpress-develop by @westonruter.


3 weeks ago
#85

Trac ticket: https://core.trac.wordpress.org/ticket/64898

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Opus 5
Used for:

  • Troubleshooting PHPStan complaining that the data_wp_bind_processor method was unused. See 1638ca3c44b7a02b7a1cf3a12233e088b1b1dcbd.

@westonruter commented on PR #12725:


3 weeks ago
#86

@luisherranz Would you please push up commits to this branch that apply your suggestions?

Another option to consider would be to replicate in PHP what JavaScript's String() does. An object becomes [object Object], an array becomes a comma join, and INF and NAN become "Infinity" and "NaN", although I'm not sure if, especially the [object Object] part, would make much sense.

IMO, it would be better to have a warning so that the author would be more readily discover their usage error.

#87 @westonruter
3 weeks ago

In 62939:

Build/Test Tools: Verify hook docblocks statically.

Add PHPStan extensions that read the docblock documenting each hook where the hook is fired. The value apply_filters() returns is typed from the first @param that docblock documents rather than mixed; core's /** This filter is documented in <file> */ convention is resolved, so a hook documented elsewhere is analyzed against its canonical docblock, including a dynamic canonical name such as "{$type}_template_hierarchy"; and two rules require every hook invocation to be documented, and to pass as many arguments as its documentation describes.

These conventions were previously enforced by review alone. A reference comment could name a file that no longer documents the hook, and a call site could pass fewer arguments than documented, which raises an ArgumentCountError in a callback registered for the documented count, or more, which drops the extra argument and leaves the documentation wrong. Both are now reported where they occur. The hook issues this surfaced in core were fixed in preceding commits.

The generated src/wp-includes/build tree is excluded from analysis, as its sources live in the Gutenberg plugin.

Developed in https://github.com/WordPress/wordpress-develop/pull/12022.
Follow-up to r61699, r62292, r62893, r62894, r62925, r62926, r62927, r62928, r62930, r62931, r62932, r62933, r62934, r62935.

Props westonruter, szepeviktor, khokansardar.
See #64898.
Fixes #65376.

@westonruter commented on PR #12725:


3 weeks ago
#88

This is a bit unexpected to me , but it also is the behavior in trunk, which I just realized.

@luisherranz I think I got it. How about 0d8e02388419a7fc21e9d08c96ef6f61f6483ec4?

#89 @westonruter
3 weeks ago

In 62944:

Interactivity API: Fix a fatal error when binding a non-scalar value.

The WP_Interactivity_API::data_wp_bind_processor() method passed the evaluated reference straight to WP_HTML_Tag_Processor::set_attribute(), which is typed string|bool, so a non-scalar value raised a TypeError inside the escaping functions: in strtr() for an ordinary attribute, and in esc_url() for a URI one. This is reachable from post content alone, with no plugin code, because data-wp-context can supply an array inline. Such a value is now rejected with _doing_it_wrong() and the attribute is left unset, as a null value already did, rather than taking down the whole page render.

An object is resolved by round-tripping it through wp_json_encode() and json_decode(), which is how the client store itself is built, so the two cannot disagree about what it serializes to. __toString() is never consulted: an object whose string form differed from its JSON form used to render a value the client immediately overwrote, and one implementing only JsonSerializable fataled even though the store was already correct. Numbers are formatted by that same encoder rather than cast, which avoids both the locale-dependent decimal separator a float cast produces before PHP 8.0 and the rounding to precision where the store uses serialize_precision. INF and NAN are scalars but JSON can represent neither, and there the cost is the page's entire state rather than one attribute, so they are rejected with a message of their own.

Parsing of directive attributes is hardened. The code touching the ::data_wp_bind_processor() method is brought to PHPStan level 10 without ignores, including the use of more specific types where available.

Developed in https://github.com/WordPress/wordpress-develop/pull/12725.
Follow-up to r57563, r61020, r62070.

Props westonruter, dmsnell, luisherranz, darerodz.
See #64898.
Fixes #65740.

@luisherranz commented on PR #12725:


3 weeks ago
#90

@luisherranz I think I got it. How about https://github.com/WordPress/wordpress-develop/commit/0d8e02388419a7fc21e9d08c96ef6f61f6483ec4?

Looks great! I'll prepare the fix to sync it in Gutenberg, thank you very much! 🙂

@luisherranz commented on PR #12725:


3 weeks ago
#91

Gutenberg PR for the syntax backport here:

#92 @westonruter
3 weeks ago

In 62965:

Build/Test Tools: Improve the constants defined for PHPStan.

Many constants were defined as empty strings, including ones that never hold an empty value in a real install. Realistic values are provided for those, matching what wp-settings.php and default-constants.php would produce, so that functions building on them can be given narrower types without the placeholder itself violating them. Constants core itself defines as empty, such as WP_DEVELOPMENT_MODE and COOKIE_DOMAIN, are left as they were.

Developed as subset of https://github.com/WordPress/wordpress-develop/pull/11851.

See #64898.

#93 @westonruter
3 weeks ago

In 62966:

Docs: Improve block asset registration docblocks.

Per the inline documentation standards, a docblock's summary belongs on its own line separated from the description, and the description should not open with "It". This is applied to register_block_script_module_id(), register_block_script_handle(), and register_block_style_handle(), together with some missing articles in the same descriptions.

Two of those descriptions no longer matched the code. register_block_script_handle() said the script is registered under an automatically generated handle, but since 6.5.0 the handle is taken from the asset file whenever one provides it, and generation is only the fallback. register_block_style_handle() said it returns the unprocessed style handle otherwise, which does not hold for the first style of a core block: that one is registered from the block's own stylesheet when separate core block assets are loaded, and skipped entirely when they are not.

The same functions gain @phpstan- annotations describing the shape of the $metadata they accept and the narrower strings they return. The shapes follow the block.json schema, which constrains only name, so the remaining fields stay plain strings; file is nullable and name optional because register_block_type_from_metadata() can reach all three functions with neither present.

Developed in https://github.com/WordPress/wordpress-develop/pull/11851.
Follow-up to r48141, r55447, r57559, r57565.

Props deepakrohilla, westonruter, sabernhardt, wildworks, audrasjb.
See #64898.
Fixes #65259.

#94 @westonruter
2 weeks ago

In 63002:

Media: Normalize unusable sizes attachment metadata.

Attachment metadata is untyped, and the sizes key is not guaranteed to be present or to hold an array. Sub-size generation can leave it out entirely, and a plugin filtering wp_get_attachment_metadata can replace it with anything. wp_save_image() validated only that the metadata itself was an array before passing $meta['sizes'] to array_merge(), so an absent or scalar value raised a TypeError and the image editor returned an HTTP 500 mid-save. wp_restore_image() had the same gap at $meta['sizes'][ $default_size ] = $data, where a string raises "Cannot use a scalar value as an array" and false is deprecated as of PHP 8.1 and an error as of PHP 9.

wp_get_attachment_metadata() now returns false whenever the metadata is not an array, on the $unfiltered path as well as after the filter, matching the documented array|false return. A sizes key holding a non-array is replaced with an empty array, so every caller can rely on the key being an array whenever it is present. The key is not invented when it is absent: audio, video and document attachments legitimately store metadata without it, and callers such as wp-admin/post.php read the metadata unfiltered in order to modify it and write it back, so normalizing there would persist into the database.

The image editor entry points fill in the missing key themselves, and wp_prepare_attachment_for_js() now checks the dimensions of the full entry alongside its filename before reading them, removing the "Undefined array key" warnings raised for a sizes array that carries no usable full size. PHPUnit coverage is added for all three functions.

Developed in https://github.com/WordPress/wordpress-develop/pull/12744.
Follow-up to r11965, r23873, r38949, r49084, r62978.

Props josephscott, westonruter, mukesh27, irozum, ugyensupport, nazmulasif.
See #65686, #64898.
Fixes #65748.

#95 @westonruter
2 weeks ago

In 63005:

Build/Test Tools: Add @phpstan-assert on assertIXRError and assertNotIXRError.

See #64898.

This ticket was mentioned in Slack in #core by adrianduffell. View the logs.


2 weeks ago

#97 @desrosj
2 weeks ago

  • Resolutionfixed
  • Status newclosed

With 7.1 RC1 due out today, I'm going to close this one out. This can be reopened if anything comes up that needs addressing for 7.1.

I've created #65817 for the 7.2 cycle.

#98 @dmsnell
12 days ago

In 63158:

Docs: Revert "Indicate absint() returns non-negative-int...".

The introduction of a return-type annotation for absint() created fatal errors in cases where the function returns a float value. This resulted when the value passed into the function is smaller than PHP_INT_MIN. Since PHP’s int type is unable to represent the magnitude of that number in the positive, it returns a float value instead.

Reverting the type annotation prevents the crashing, but additional follow-up is warranted to ensure that the function produces the expected return types.

Developed in: https://github.com/WordPress/wordpress-develop/pull/12940
Discussed in: https://core.trac.wordpress.org/ticket/65826

Follow-up to [62647].

Props dmsnell, josephscott, westonruter.
Fixes #65826. See #64898.

#99 @westonruter
4 days ago

PR which fixes static analysis errors in the 7.1 release cycle which had been baselined: https://github.com/WordPress/wordpress-develop/pull/13064

Note: See TracTickets for help on using tickets.