Commit 9491ed4ce8 for wordpress.org

commit 9491ed4ce874163f4034affb2a388b0c7f16a4c2
Author: westonruter <westonruter@git.wordpress.org>
Date:   Mon Oct 19 17:40:58 2026 +0000

    Code Quality: Narrow types for REST requests and responses.

    Adds array shapes and narrowed types to `WP_HTTP_Response`, `WP_REST_Request` and `WP_REST_Response`, and to the parts of `WP_REST_Server` that pass data to them, reusing the route handler and endpoint argument aliases the server already defines. Several existing types were wrong rather than merely loose, and are corrected:

    * A request's body is null until one is set.
    * A request's JSON params decode to a scalar for a JSON body that is not an object or array.
    * A request's headers hold lists of values.
    * A file param may describe a failed upload, or a field named with brackets such as `files[]`.

    The status code is typed `int<100, 599>`, and `absint()` gains a template conditional return so that an argument already known to be a non-negative integer keeps its narrower type through it. The one call where that type cannot be inferred, in the deprecated numeric branch of `wp_get_recent_posts()`, is added to the PHPStan baseline. The no-cache check for a 4xx response to a method-overridden request now compares the status numerically instead of passing an integer to `str_starts_with()`, which removes another baseline entry, and a test now covers that path.

    The narrower types also expose existing bugs that are left for follow-up: a scalar JSON body makes `WP_REST_Request::set_param()` fatal and `WP_REST_Request::sanitize_params()` warn, and uploading to the media endpoint with a `file[]` field makes `WP_REST_Attachments_Controller::create_item()` throw a `TypeError`.

    Developed in https://github.com/WordPress/wordpress-develop/pull/14471.
    Follow-up to r34928, r56834, r57126, r62618, r63646, r63756.

    Props westonruter, apermo.
    See #65817.

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


    git-svn-id: http://core.svn.wordpress.org/trunk@63677 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/class-wp-http-response.php b/wp-includes/class-wp-http-response.php
index d102877885..d9d8d871c4 100644
--- a/wp-includes/class-wp-http-response.php
+++ b/wp-includes/class-wp-http-response.php
@@ -27,7 +27,7 @@ class WP_HTTP_Response {
 	 * Response headers.
 	 *
 	 * @since 4.4.0
-	 * @var array
+	 * @var array<string, string>
 	 */
 	public $headers;

@@ -36,6 +36,7 @@ class WP_HTTP_Response {
 	 *
 	 * @since 4.4.0
 	 * @var int
+	 * @phpstan-var int<100, 599>
 	 */
 	public $status;

@@ -44,9 +45,11 @@ class WP_HTTP_Response {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param mixed $data    Response data. Default null.
-	 * @param int   $status  Optional. HTTP status code. Default 200.
-	 * @param array $headers Optional. HTTP header map. Default empty array.
+	 * @param mixed                 $data    Response data. Default null.
+	 * @param int                   $status  Optional. HTTP status code. Default 200.
+	 * @param array<string, string> $headers Optional. HTTP header map. Default empty array.
+	 *
+	 * @phpstan-param int<100, 599> $status
 	 */
 	public function __construct( $data = null, $status = 200, $headers = array() ) {
 		$this->set_data( $data );
@@ -59,7 +62,7 @@ class WP_HTTP_Response {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Map of header name to header value.
+	 * @return array<string, string> Map of header name to header value.
 	 */
 	public function get_headers() {
 		return $this->headers;
@@ -70,7 +73,7 @@ class WP_HTTP_Response {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $headers Map of header name to header value.
+	 * @param array<string, string> $headers Map of header name to header value.
 	 */
 	public function set_headers( $headers ) {
 		$this->headers = $headers;
@@ -100,6 +103,8 @@ class WP_HTTP_Response {
 	 * @since 4.4.0
 	 *
 	 * @return int The 3-digit HTTP status code.
+	 *
+	 * @phpstan-return int<100, 599>
 	 */
 	public function get_status() {
 		return $this->status;
@@ -111,6 +116,8 @@ class WP_HTTP_Response {
 	 * @since 4.4.0
 	 *
 	 * @param int $code HTTP status.
+	 *
+	 * @phpstan-param int<100, 599> $code
 	 */
 	public function set_status( $code ) {
 		$this->status = absint( $code );
diff --git a/wp-includes/load.php b/wp-includes/load.php
index f736afb5b6..ecd4220e5a 100644
--- a/wp-includes/load.php
+++ b/wp-includes/load.php
@@ -1538,7 +1538,10 @@ function is_multisite() {
  *
  * @param mixed $maybeint Data you wish to have converted to a non-negative integer.
  * @return int A non-negative integer.
- * @phpstan-return non-negative-int
+ *
+ * @phpstan-template T
+ * @phpstan-param T $maybeint
+ * @phpstan-return (T is non-negative-int ? T : non-negative-int)
  */
 function absint( $maybeint ): int {
 	if ( is_float( $maybeint ) ) {
diff --git a/wp-includes/rest-api/class-wp-rest-request.php b/wp-includes/rest-api/class-wp-rest-request.php
index 6e2619afeb..7773a40aaf 100644
--- a/wp-includes/rest-api/class-wp-rest-request.php
+++ b/wp-includes/rest-api/class-wp-rest-request.php
@@ -25,6 +25,54 @@
  * @since 4.4.0
  *
  * @link https://www.php.net/manual/en/class.arrayaccess.php
+ *
+ * @phpstan-import-type Endpoint_Arg from WP_REST_Server
+ *
+ * @phpstan-type Attributes array{
+ *     methods?: array<uppercase-string, true>,
+ *     callback?: callable|null,
+ *     permission_callback?: callable|null,
+ *     validate_callback?: callable|null,
+ *     args?: array<non-empty-string, Endpoint_Arg>,
+ *     accept_json?: bool,
+ *     accept_raw?: bool,
+ *     show_in_index?: bool,
+ *     allow_batch?: array{v1?: bool}|false,
+ *     ...
+ * }
+ * @phpstan-type File_Param array{
+ *     name: non-empty-string,
+ *     type: string,
+ *     size: non-negative-int,
+ *     tmp_name: non-empty-string,
+ *     error: 0,
+ *     full_path?: non-empty-string,
+ * }|array{
+ *     name: string,
+ *     type: string,
+ *     size: non-negative-int,
+ *     tmp_name: string,
+ *     error: int<1, 8>,
+ *     full_path?: string,
+ * }|array{
+ *     name: array<mixed>,
+ *     type: array<mixed>,
+ *     size: array<mixed>,
+ *     tmp_name: array<mixed>,
+ *     error: array<mixed>,
+ *     full_path?: array<mixed>,
+ * }
+ * @phpstan-type Parameter_Type 'URL'|'GET'|'POST'|'FILES'|'JSON'|'defaults'
+ * @phpstan-type Params array{
+ *     URL: array<string, mixed>,
+ *     GET: array<mixed>,
+ *     POST: array<mixed>,
+ *     FILES: array<string, File_Param>,
+ *     JSON: array<mixed>|string|int|float|bool|null,
+ *     defaults: array<string, mixed>,
+ * }
+ *
+ * @phpstan-implements ArrayAccess<string, mixed>
  */
 #[AllowDynamicProperties]
 class WP_REST_Request implements ArrayAccess {
@@ -34,6 +82,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 * @var string
+	 * @phpstan-var uppercase-string
 	 */
 	protected $method = '';

@@ -44,7 +93,8 @@ class WP_REST_Request implements ArrayAccess {
 	 * superglobals when being created from the global scope.
 	 *
 	 * @since 4.4.0
-	 * @var array Contains GET, POST and FILES keys mapping to arrays of data.
+	 * @var array<string, mixed> Contains GET, POST and FILES keys mapping to arrays of data.
+	 * @phpstan-var Params
 	 */
 	protected $params;

@@ -52,7 +102,8 @@ class WP_REST_Request implements ArrayAccess {
 	 * HTTP headers for the request.
 	 *
 	 * @since 4.4.0
-	 * @var array Map of key to value. Key is always lowercase, as per HTTP specification.
+	 * @var array<string, string[]> Map of key to list of values. Key is always lowercase, as per HTTP specification.
+	 * @phpstan-var array<lowercase-string, string[]>
 	 */
 	protected $headers = array();

@@ -60,7 +111,7 @@ class WP_REST_Request implements ArrayAccess {
 	 * Body data.
 	 *
 	 * @since 4.4.0
-	 * @var string Binary data from the request.
+	 * @var string|null Binary data from the request, or null if no body has been set.
 	 */
 	protected $body = null;

@@ -79,7 +130,8 @@ class WP_REST_Request implements ArrayAccess {
 	 * containing the callback as well as the valid methods for the route.
 	 *
 	 * @since 4.4.0
-	 * @var array Attributes for the request.
+	 * @var array<string, mixed> Attributes for the request.
+	 * @phpstan-var Attributes
 	 */
 	protected $attributes = array();

@@ -106,9 +158,11 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param string $method     Optional. Request method. Default empty.
-	 * @param string $route      Optional. Request route. Default empty.
-	 * @param array  $attributes Optional. Request attributes. Default empty array.
+	 * @param string               $method     Optional. Request method. Default empty.
+	 * @param string               $route      Optional. Request route. Default empty.
+	 * @param array<string, mixed> $attributes Optional. Request attributes. Default empty array.
+	 *
+	 * @phpstan-param Attributes $attributes
 	 */
 	public function __construct( $method = '', $route = '', $attributes = array() ) {
 		$this->params = array(
@@ -134,6 +188,8 @@ class WP_REST_Request implements ArrayAccess {
 	 * @since 4.4.0
 	 *
 	 * @return string HTTP method.
+	 *
+	 * @phpstan-return uppercase-string
 	 */
 	public function get_method() {
 		return $this->method;
@@ -155,7 +211,9 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Map of key to value. Key is always lowercase, as per HTTP specification.
+	 * @return array<string, string[]> Map of key to list of values. Key is always lowercase, as per HTTP specification.
+	 *
+	 * @phpstan-return array<lowercase-string, string[]>
 	 */
 	public function get_headers() {
 		return $this->headers;
@@ -190,6 +248,8 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @param string $key Header name.
 	 * @return string Canonicalized name.
+	 *
+	 * @phpstan-return lowercase-string
 	 */
 	public static function canonicalize_header_name( $key ) {
 		$key = strtolower( $key );
@@ -226,7 +286,7 @@ class WP_REST_Request implements ArrayAccess {
 	 * @since 4.4.0
 	 *
 	 * @param string $key Header name, will be canonicalized to lowercase.
-	 * @return array|null List of string values if set, null otherwise.
+	 * @return string[]|null List of string values if set, null otherwise.
 	 */
 	public function get_header_as_array( $key ) {
 		$key = static::canonicalize_header_name( $key );
@@ -243,8 +303,8 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param string $key   Header name.
-	 * @param string $value Header value, or list of values.
+	 * @param string          $key   Header name.
+	 * @param string|string[] $value Header value, or list of values.
 	 */
 	public function set_header( $key, $value ) {
 		$key   = static::canonicalize_header_name( $key );
@@ -258,8 +318,8 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param string $key   Header name.
-	 * @param string $value Header value, or list of values.
+	 * @param string          $key   Header name.
+	 * @param string|string[] $value Header value, or list of values.
 	 */
 	public function add_header( $key, $value ) {
 		$key   = static::canonicalize_header_name( $key );
@@ -289,8 +349,8 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $headers  Map of header name to value.
-	 * @param bool  $override If true, replace the request's headers. Otherwise, merge with existing.
+	 * @param array<string, string|string[]> $headers  Map of header name to value, or list of values.
+	 * @param bool                           $override If true, replace the request's headers. Otherwise, merge with existing.
 	 */
 	public function set_headers( $headers, $override = true ) {
 		if ( true === $override ) {
@@ -307,9 +367,16 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array|null Map containing 'value' and 'parameters' keys
-	 *                    or null when no valid Content-Type header was
-	 *                    available.
+	 * @return array|null {
+	 *     The parsed Content-Type header, or null when no valid Content-Type
+	 *     header was available.
+	 *
+	 *     @type string $value      Media type, lowercased, such as `application/json`.
+	 *     @type string $type       Top-level type, such as `application`.
+	 *     @type string $subtype    Subtype, such as `json`.
+	 *     @type string $parameters Parameters following the media type, such as
+	 *                              `charset=UTF-8`. Empty string if none.
+	 * }
 	 */
 	public function get_content_type() {
 		$value = $this->get_header( 'Content-Type' );
@@ -357,6 +424,8 @@ class WP_REST_Request implements ArrayAccess {
 	 * @since 4.4.0
 	 *
 	 * @return string[] Array of types to check, in order of priority.
+	 *
+	 * @phpstan-return list<Parameter_Type>
 	 */
 	protected function get_parameter_order() {
 		$order = array();
@@ -393,6 +462,8 @@ class WP_REST_Request implements ArrayAccess {
 		 *
 		 * @param string[]        $order   Array of types to check, in order of priority.
 		 * @param WP_REST_Request $request The request object.
+		 *
+		 * @phpstan-param list<Parameter_Type> $order
 		 */
 		return apply_filters( 'rest_request_parameter_order', $order, $this );
 	}
@@ -477,7 +548,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Map of key to value.
+	 * @return array<mixed> Map of key to value.
 	 */
 	public function get_params() {
 		$order = $this->get_parameter_order();
@@ -509,7 +580,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Parameter map of key to value.
+	 * @return array<string, mixed> Parameter map of key to value.
 	 */
 	public function get_url_params() {
 		return $this->params['URL'];
@@ -522,7 +593,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $params Parameter map of key to value.
+	 * @param array<string, mixed> $params Parameter map of key to value.
 	 */
 	public function set_url_params( $params ) {
 		$this->params['URL'] = $params;
@@ -535,7 +606,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Parameter map of key to value.
+	 * @return array<mixed> Parameter map of key to value.
 	 */
 	public function get_query_params() {
 		return $this->params['GET'];
@@ -548,7 +619,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $params Parameter map of key to value.
+	 * @param array<mixed> $params Parameter map of key to value.
 	 */
 	public function set_query_params( $params ) {
 		$this->params['GET'] = $params;
@@ -561,7 +632,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Parameter map of key to value.
+	 * @return array<mixed> Parameter map of key to value.
 	 */
 	public function get_body_params() {
 		return $this->params['POST'];
@@ -574,7 +645,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $params Parameter map of key to value.
+	 * @param array<mixed> $params Parameter map of key to value.
 	 */
 	public function set_body_params( $params ) {
 		$this->params['POST'] = $params;
@@ -587,16 +658,9 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Parameter map of key to value.
+	 * @return array<string, array<string, mixed>> Parameter map of key to value.
 	 *
-	 * @phpstan-return array<string, array{
-	 *                                   name: non-empty-string,
-	 *                                   type: non-empty-string,
-	 *                                   size: non-negative-int,
-	 *                                   tmp_name: non-empty-string,
-	 *                                   error: int<0, 8>,
-	 *                                   full_path?: non-empty-string,
-	 *                               }>
+	 * @phpstan-return array<string, File_Param>
 	 */
 	public function get_file_params() {
 		return $this->params['FILES'];
@@ -609,16 +673,9 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $params Parameter map of key to value.
+	 * @param array<string, array<string, mixed>> $params Parameter map of key to value.
 	 *
-	 * @phpstan-param array<string, array{
-	 *                                  name: non-empty-string,
-	 *                                  type: non-empty-string,
-	 *                                  size: non-negative-int,
-	 *                                  tmp_name: non-empty-string,
-	 *                                  error: int<0, 8>,
-	 *                                  full_path?: non-empty-string,
-	 *                              }> $params
+	 * @phpstan-param array<string, File_Param> $params
 	 */
 	public function set_file_params( $params ) {
 		$this->params['FILES'] = $params;
@@ -631,7 +688,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Parameter map of key to value.
+	 * @return array<string, mixed> Parameter map of key to value.
 	 */
 	public function get_default_params() {
 		return $this->params['defaults'];
@@ -644,7 +701,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $params Parameter map of key to value.
+	 * @param array<string, mixed> $params Parameter map of key to value.
 	 */
 	public function set_default_params( $params ) {
 		$this->params['defaults'] = $params;
@@ -655,7 +712,7 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return string Binary data from the request body.
+	 * @return string|null Binary data from the request body, or null if no body has been set.
 	 */
 	public function get_body() {
 		return $this->body;
@@ -682,7 +739,9 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Parameter map of key to value.
+	 * @return array<mixed>|string|int|float|bool|null Parameter map of key to value, or null if the request
+	 *                                                 has no valid JSON body. A JSON body that is not an
+	 *                                                 object or array decodes to the scalar it contains.
 	 */
 	public function get_json_params() {
 		// Ensure the parameters have been parsed out.
@@ -803,7 +862,9 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array Attributes for the request.
+	 * @return array<string, mixed> Attributes for the request.
+	 *
+	 * @phpstan-return Attributes
 	 */
 	public function get_attributes() {
 		return $this->attributes;
@@ -814,7 +875,9 @@ class WP_REST_Request implements ArrayAccess {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $attributes Attributes for the request.
+	 * @param array<string, mixed> $attributes Attributes for the request.
+	 *
+	 * @phpstan-param Attributes $attributes
 	 */
 	public function set_attributes( $attributes ) {
 		$this->attributes = $attributes;
diff --git a/wp-includes/rest-api/class-wp-rest-response.php b/wp-includes/rest-api/class-wp-rest-response.php
index ddb1fb27f4..7624e01d61 100644
--- a/wp-includes/rest-api/class-wp-rest-response.php
+++ b/wp-includes/rest-api/class-wp-rest-response.php
@@ -13,6 +13,33 @@
  * @since 4.4.0
  *
  * @see WP_HTTP_Response
+ *
+ * @phpstan-import-type Route_Handler from WP_REST_Server
+ *
+ * @phpstan-type Target_Hints array{
+ *     allow?: list<uppercase-string>,
+ *     ...<lowercase-string, mixed>
+ * }
+ * @phpstan-type Link_Attributes array{
+ *     embeddable?: bool,
+ *     targetHints?: Target_Hints,
+ *     ...<string, mixed>
+ * }
+ * @phpstan-type Link array{
+ *     href: non-falsy-string,
+ *     attributes: Link_Attributes,
+ * }
+ * @phpstan-type Link_Input array{
+ *     href: non-falsy-string,
+ *     embeddable?: bool,
+ *     targetHints?: array<string, mixed>, // Not narrowed like in Link_Attributes, since PHPStan merges this shape with its list form in add_links(). The merged attributes passed to add_link() there are only accepted while they include `mixed`; with the nested shape, the call would be an argument.type error even when `mixed` is not checked.
+ *     ...<string, mixed>
+ * }
+ * @phpstan-type Curie array{
+ *     name: string, // An empty name is a valid empty CURIE prefix, which WP_REST_Server::get_compact_response_links() handles consistently.
+ *     href: non-falsy-string,
+ *     templated?: bool,
+ * }
  */
 class WP_REST_Response extends WP_HTTP_Response {

@@ -20,7 +47,8 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 * Links related to the response.
 	 *
 	 * @since 4.4.0
-	 * @var array
+	 * @var array<string, array<string, mixed>[]> Map of link relation to list of links.
+	 * @phpstan-var array<non-falsy-string, array<Link>>
 	 */
 	protected $links = array();

@@ -36,7 +64,8 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 * The handler that was used to create the response.
 	 *
 	 * @since 4.4.0
-	 * @var null|array
+	 * @var array<string, mixed>|null
+	 * @phpstan-var Route_Handler|null
 	 */
 	protected $matched_handler = null;

@@ -50,10 +79,14 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 * @link https://tools.ietf.org/html/rfc5988
 	 * @link https://www.iana.org/assignments/link-relations/link-relations.xml
 	 *
-	 * @param string $rel        Link relation. Either an IANA registered type,
-	 *                           or an absolute URL.
-	 * @param string $href       Target URI for the link.
-	 * @param array  $attributes Optional. Link parameters to send along with the URL. Default empty array.
+	 * @param string               $rel        Link relation. Either an IANA registered type,
+	 *                                         or an absolute URL.
+	 * @param string               $href       Target URI for the link.
+	 * @param array<string, mixed> $attributes Optional. Link parameters to send along with the URL. Default empty array.
+	 *
+	 * @phpstan-param non-falsy-string $rel
+	 * @phpstan-param non-falsy-string $href
+	 * @phpstan-param Link_Attributes  $attributes
 	 */
 	public function add_link( $rel, $href, $attributes = array() ) {
 		if ( empty( $this->links[ $rel ] ) ) {
@@ -79,6 +112,8 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 * @param string      $rel  Link relation. Either an IANA registered type, or an absolute URL.
 	 * @param string|null $href Optional. Only remove links for the relation matching the given href.
 	 *                          Default null.
+	 *
+	 * @phpstan-param non-falsy-string $rel
 	 */
 	public function remove_link( $rel, $href = null ) {
 		if ( ! isset( $this->links[ $rel ] ) ) {
@@ -106,7 +141,9 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $links Map of link relation to list of links.
+	 * @param array<string, array<mixed>> $links Map of link relation to a link's attributes, or to a list of these.
+	 *
+	 * @phpstan-param array<non-falsy-string, Link_Input|list<Link_Input>> $links
 	 */
 	public function add_links( $links ) {
 		foreach ( $links as $rel => $set ) {
@@ -126,7 +163,9 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return array List of links.
+	 * @return array<string, array<string, mixed>[]> Map of link relation to list of links.
+	 *
+	 * @phpstan-return array<non-falsy-string, array<Link>>
 	 */
 	public function get_links() {
 		return $this->links;
@@ -142,10 +181,12 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 * @link https://tools.ietf.org/html/rfc5988
 	 * @link https://www.iana.org/assignments/link-relations/link-relations.xml
 	 *
-	 * @param string $rel   Link relation. Either an IANA registered type, or an absolute URL.
-	 * @param string $link  Target IRI for the link.
-	 * @param array  $other Optional. Other parameters to send, as an associative array.
-	 *                      Default empty array.
+	 * @param string                $rel   Link relation. Either an IANA registered type, or an absolute URL.
+	 * @param string                $link  Target IRI for the link.
+	 * @param array<string, string> $other Optional. Other parameters to send, as an associative array.
+	 *                                     Default empty array.
+	 *
+	 * @phpstan-param non-falsy-string $rel
 	 */
 	public function link_header( $rel, $link, $other = array() ) {
 		$header = '<' . $link . '>; rel="' . $rel . '"';
@@ -187,7 +228,9 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @return null|array The handler that was used to create the response.
+	 * @return array<string, mixed>|null The handler that was used to create the response.
+	 *
+	 * @phpstan-return Route_Handler|null
 	 */
 	public function get_matched_handler() {
 		return $this->matched_handler;
@@ -198,7 +241,9 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $handler The matched handler.
+	 * @param array<string, mixed> $handler The matched handler.
+	 *
+	 * @phpstan-param Route_Handler $handler
 	 */
 	public function set_matched_handler( $handler ) {
 		$this->matched_handler = $handler;
@@ -250,7 +295,9 @@ class WP_REST_Response extends WP_HTTP_Response {
 	 *
 	 * @since 4.5.0
 	 *
-	 * @return array Compact URIs.
+	 * @return array<string, string|bool>[] Compact URIs.
+	 *
+	 * @phpstan-return array<Curie>
 	 */
 	public function get_curies() {
 		$curies = array(
@@ -284,7 +331,9 @@ class WP_REST_Response extends WP_HTTP_Response {
 		 *
 		 * @since 4.5.0
 		 *
-		 * @param array $additional Additional CURIEs to register with the REST API.
+		 * @param array<string, string|bool>[] $additional Additional CURIEs to register with the REST API.
+		 *
+		 * @phpstan-param array<Curie> $additional
 		 */
 		$additional = apply_filters( 'rest_response_link_curies', array() );

diff --git a/wp-includes/rest-api/class-wp-rest-server.php b/wp-includes/rest-api/class-wp-rest-server.php
index 48eb61f494..0603f79497 100644
--- a/wp-includes/rest-api/class-wp-rest-server.php
+++ b/wp-includes/rest-api/class-wp-rest-server.php
@@ -20,6 +20,8 @@
  *
  * @since 4.4.0
  *
+ * @phpstan-import-type Target_Hints from WP_REST_Response
+ *
  * @phpstan-type Endpoint_Arg array{
  *     required?: bool,
  *     default?: mixed,
@@ -32,6 +34,7 @@
  *     methods: array<uppercase-string, true>,
  *     callback?: callable|null,
  *     permission_callback?: callable|null,
+ *     validate_callback?: callable|null,
  *     args: array<non-empty-string, Endpoint_Arg>,
  *     accept_json: bool,
  *     accept_raw: bool,
@@ -270,6 +273,8 @@ class WP_REST_Server {
 	 * @param string   $message Human-readable message.
 	 * @param int|null $status  Optional. HTTP status code to send. Default null.
 	 * @return string JSON representation of the error.
+	 *
+	 * @phpstan-param int<100, 599>|null $status
 	 */
 	protected function json_error( $code, $message, $status = null ) {
 		if ( $status ) {
@@ -531,7 +536,7 @@ class WP_REST_Server {
 		 * Send no-cache headers if $send_no_cache_headers is true,
 		 * OR if the HTTP_X_HTTP_METHOD_OVERRIDE is used but resulted a 4xx response code.
 		 */
-		if ( $send_no_cache_headers || ( true === $method_overridden && str_starts_with( $code, '4' ) ) ) {
+		if ( $send_no_cache_headers || ( true === $method_overridden && $code >= 400 && $code < 500 ) ) {
 			foreach ( wp_get_nocache_headers() as $header => $header_value ) {
 				if ( empty( $header_value ) ) {
 					$this->remove_header( $header );
@@ -701,8 +706,17 @@ class WP_REST_Server {
 	 *
 	 * @since 6.7.0
 	 *
-	 * @param array $link The link to get target hints for.
-	 * @return array|null
+	 * @param array<string, mixed> $link The link to get target hints for.
+	 * @return array<string, string[]>|null Map of hint name to list of values, which is empty if the route
+	 *                                      allows no methods. Null if the link already has an `allow` hint,
+	 *                                      or its URL does not resolve to a valid REST API request.
+	 *
+	 * @phpstan-param array{
+	 *     href: non-falsy-string,
+	 *     targetHints?: Target_Hints,
+	 *     ...<string, mixed>
+	 * } $link
+	 * @phpstan-return array<lowercase-string, list<string>>|null
 	 */
 	protected static function get_target_hints_for_link( $link ) {
 		// Prefer targetHints that were specifically designated by the developer.
@@ -1213,7 +1227,9 @@ class WP_REST_Server {
 	 * @since 5.6.0
 	 *
 	 * @param WP_REST_Request $request The request object.
-	 * @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
+	 * @return array<int, string|array<string, mixed>>|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
+	 *
+	 * @phpstan-return array{non-empty-string, Route_Handler}|WP_Error
 	 */
 	protected function match_request_to_handler( $request ) {
 		$method = $request->get_method();
@@ -1294,11 +1310,13 @@ class WP_REST_Server {
 	 * @access private
 	 * @since 5.6.0
 	 *
-	 * @param WP_REST_Request $request  The request object.
-	 * @param string          $route    The matched route regex.
-	 * @param array           $handler  The matched route handler.
-	 * @param WP_Error|null   $response The current error object if any.
+	 * @param WP_REST_Request      $request  The request object.
+	 * @param string               $route    The matched route regex.
+	 * @param array<string, mixed> $handler  The matched route handler.
+	 * @param WP_Error|null        $response The current error object if any.
 	 * @return WP_REST_Response
+	 *
+	 * @phpstan-param Route_Handler $handler
 	 */
 	protected function respond_to_request( $request, $route, $handler, $response ) {
 		/**
@@ -1315,8 +1333,10 @@ class WP_REST_Server {
 		 *
 		 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
 		 *                                                                   Usually a WP_REST_Response or WP_Error.
-		 * @param array                                            $handler  Route handler used for the request.
+		 * @param array<string, mixed>                             $handler  Route handler used for the request.
 		 * @param WP_REST_Request                                  $request  Request used to generate the response.
+		 *
+		 * @phpstan-param Route_Handler $handler
 		 */
 		$response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );

@@ -1344,10 +1364,12 @@ class WP_REST_Server {
 			 * @since 4.4.0
 			 * @since 4.5.0 Added `$route` and `$handler` parameters.
 			 *
-			 * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
-			 * @param WP_REST_Request $request         Request used to generate the response.
-			 * @param string          $route           Route matched for the request.
-			 * @param array           $handler         Route handler used for the request.
+			 * @param mixed                $dispatch_result Dispatch result, will be used if not empty.
+			 * @param WP_REST_Request      $request         Request used to generate the response.
+			 * @param string               $route           Route matched for the request.
+			 * @param array<string, mixed> $handler         Route handler used for the request.
+			 *
+			 * @phpstan-param Route_Handler $handler
 			 */
 			$dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );

@@ -1377,8 +1399,10 @@ class WP_REST_Server {
 		 *
 		 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
 		 *                                                                   Usually a WP_REST_Response or WP_Error.
-		 * @param array                                            $handler  Route handler used for the request.
+		 * @param array<string, mixed>                             $handler  Route handler used for the request.
 		 * @param WP_REST_Request                                  $request  Request used to generate the response.
+		 *
+		 * @phpstan-param Route_Handler $handler
 		 */
 		$response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );

@@ -1987,6 +2011,8 @@ class WP_REST_Server {
 	 * @since 4.4.0
 	 *
 	 * @param int $code HTTP status.
+	 *
+	 * @phpstan-param int<100, 599> $code
 	 */
 	protected function set_status( $code ) {
 		status_header( $code );
@@ -2017,7 +2043,7 @@ class WP_REST_Server {
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param array $headers Map of header name to header value.
+	 * @param array<string, string> $headers Map of header name to header value.
 	 */
 	public function send_headers( $headers ) {
 		foreach ( $headers as $key => $value ) {
diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
index 442e100f3e..61087ef598 100644
--- a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
@@ -1683,6 +1683,8 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 	 * @param WP_Comment      $comment Comment object.
 	 * @param WP_REST_Request $request Request object.
 	 * @return string[] List of link relations.
+	 *
+	 * @phpstan-return array<non-falsy-string>
 	 */
 	protected function get_available_actions( $comment, $request ) {
 		if ( 'edit' !== $request['context'] ) {
diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
index 190eea6b20..b2429f1f48 100644
--- a/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
@@ -1013,6 +1013,8 @@ class WP_REST_Templates_Controller extends WP_REST_Controller {
 	 * @since 5.8.0
 	 *
 	 * @return string[] List of link relations.
+	 *
+	 * @phpstan-return array<non-falsy-string>
 	 */
 	protected function get_available_actions() {
 		$rels = array();
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 4018ec1656..6ac6f367cc 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-64526';
+$wp_version = '7.2-alpha-64527';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.