From 2a3f13dd6466b6b0910b027ec4005838e1bc7354 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 19:37:31 -0800 Subject: [PATCH 1/8] Resolve isset.variable error since $_POST global is always set --- src/wp-admin/includes/class-custom-image-header.php | 2 +- tests/phpstan/baselines/isset.variable.neon | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-admin/includes/class-custom-image-header.php b/src/wp-admin/includes/class-custom-image-header.php index 97a1a16052d12..c1816ffae2d9c 100644 --- a/src/wp-admin/includes/class-custom-image-header.php +++ b/src/wp-admin/includes/class-custom-image-header.php @@ -841,7 +841,7 @@ public function step_2() { $file = get_attached_file( $attachment_id, true ); $url = wp_get_attachment_image_src( $attachment_id, 'full' ); $url = $url[0]; - } elseif ( isset( $_POST ) ) { + } else { $data = $this->step_2_manage_upload(); $attachment_id = $data['attachment_id']; $file = $data['file']; diff --git a/tests/phpstan/baselines/isset.variable.neon b/tests/phpstan/baselines/isset.variable.neon index f14ef0755fdb9..2f07bfbf32821 100644 --- a/tests/phpstan/baselines/isset.variable.neon +++ b/tests/phpstan/baselines/isset.variable.neon @@ -18,11 +18,6 @@ parameters: ignoreErrors: - - - message: '#^Variable \$_POST in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable - count: 1 - path: ../../../src/wp-admin/includes/class-custom-image-header.php - message: '#^Variable \$stylesheet in isset\(\) always exists and is not nullable\.$#' identifier: isset.variable From daebf1d358d7ada80337cb3341113f97a6222a8e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Aug 2026 13:35:32 -0700 Subject: [PATCH 2/8] Resolve isset.variable error in load_template() --- src/wp-includes/template.php | 2 +- tests/phpstan/baselines/isset.variable.neon | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 6ec1934f866ec..cc0fbc4ae21bc 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -795,7 +795,7 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { extract( $wp_query->query_vars, EXTR_SKIP ); } - if ( isset( $s ) ) { + if ( isset( $s ) ) { // @phpstan-ignore isset.variable (It's extracted from query vars.) $s = esc_attr( $s ); // @phpstan-ignore variable.undefined (It's extracted from query vars.) } diff --git a/tests/phpstan/baselines/isset.variable.neon b/tests/phpstan/baselines/isset.variable.neon index 2f07bfbf32821..87a4940854880 100644 --- a/tests/phpstan/baselines/isset.variable.neon +++ b/tests/phpstan/baselines/isset.variable.neon @@ -38,8 +38,3 @@ parameters: identifier: isset.variable count: 1 path: ../../../src/wp-includes/class-wp-oembed.php - - - message: '#^Variable \$s in isset\(\) is never defined\.$#' - identifier: isset.variable - count: 1 - path: ../../../src/wp-includes/template.php From 2adf148ccc17c09e5aa3d53f03b2b503d072957b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Aug 2026 13:41:25 -0700 Subject: [PATCH 3/8] Fix underlying issue with variable extraction eliminating need to ignore errors --- src/wp-includes/class-wp-query.php | 4 ++++ src/wp-includes/template.php | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 228691d26d12b..dd7acecf677d1 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -31,6 +31,10 @@ class WP_Query { * * @since 1.5.0 * @var array + * @phpstan-var array{ + * s?: string, + * ... + * } */ public $query_vars = array(); diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index cc0fbc4ae21bc..eaa665c2f2d99 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -782,7 +782,8 @@ function locate_template( $template_names, $load = false, $load_once = true, $ar function load_template( $_template_file, $load_once = true, $args = array() ) { global $posts, $post, $wp_did_header, $wp_query, $wp_rewrite, $wpdb, $wp_version, $wp, $id, $comment, $user_ID; - if ( is_array( $wp_query->query_vars ) ) { + $query_vars = $wp_query->query_vars; + if ( is_array( $query_vars ) ) { /* * This use of extract() cannot be removed. There are many possible ways that * templates could depend on variables that it creates existing, and no way to @@ -792,11 +793,11 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { * function variables cannot be overwritten. */ // phpcs:ignore WordPress.PHP.DontExtract.extract_extract - extract( $wp_query->query_vars, EXTR_SKIP ); + extract( $query_vars, EXTR_SKIP ); } - if ( isset( $s ) ) { // @phpstan-ignore isset.variable (It's extracted from query vars.) - $s = esc_attr( $s ); // @phpstan-ignore variable.undefined (It's extracted from query vars.) + if ( isset( $s ) ) { + $s = esc_attr( $s ); } /** From d1f9f0480a83aacccd97fbc0cfe3b1befc9a5643 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Aug 2026 13:47:08 -0700 Subject: [PATCH 4/8] Fix isset.variable error in WP_oEmbed::_parse_xml() --- src/wp-includes/class-wp-oembed.php | 3 ++- tests/phpstan/baselines/isset.variable.neon | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-oembed.php b/src/wp-includes/class-wp-oembed.php index 39029bd9a2b8d..b1a23ff66144d 100644 --- a/src/wp-includes/class-wp-oembed.php +++ b/src/wp-includes/class-wp-oembed.php @@ -674,6 +674,7 @@ private function _parse_xml( $response_body ) { return false; } + $loader = null; if ( PHP_VERSION_ID < 80000 ) { /* * This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading @@ -688,7 +689,7 @@ private function _parse_xml( $response_body ) { libxml_use_internal_errors( $errors ); - if ( PHP_VERSION_ID < 80000 && isset( $loader ) ) { + if ( PHP_VERSION_ID < 80000 ) { // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated libxml_disable_entity_loader( $loader ); } diff --git a/tests/phpstan/baselines/isset.variable.neon b/tests/phpstan/baselines/isset.variable.neon index 87a4940854880..dd0822d9be455 100644 --- a/tests/phpstan/baselines/isset.variable.neon +++ b/tests/phpstan/baselines/isset.variable.neon @@ -33,8 +33,3 @@ parameters: identifier: isset.variable count: 1 path: ../../../src/wp-includes/class-wp-block-parser.php - - - message: '#^Variable \$loader in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable - count: 1 - path: ../../../src/wp-includes/class-wp-oembed.php From c6d055cd05c6d80d98f1bf0dc403d5112baa895b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Aug 2026 13:51:41 -0700 Subject: [PATCH 5/8] Fix isset( $_POST ) error in get_media_item() --- src/wp-admin/includes/media.php | 2 +- tests/phpstan/baselines/isset.variable.neon | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 6c50a1daba4fd..ce25f0ab26b08 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -1773,7 +1773,7 @@ function get_media_item( $attachment_id, $args = null ) { if ( isset( $_GET['post_id'] ) ) { $calling_post_id = absint( $_GET['post_id'] ); - } elseif ( isset( $_POST ) && count( $_POST ) ) {// Like for async-upload where $_GET['post_id'] isn't set. + } elseif ( ! empty( $_POST ) ) {// Like for async-upload where $_GET['post_id'] isn't set. $calling_post_id = $post->post_parent; } diff --git a/tests/phpstan/baselines/isset.variable.neon b/tests/phpstan/baselines/isset.variable.neon index dd0822d9be455..9e64459b360f8 100644 --- a/tests/phpstan/baselines/isset.variable.neon +++ b/tests/phpstan/baselines/isset.variable.neon @@ -23,11 +23,6 @@ parameters: identifier: isset.variable count: 1 path: ../../../src/wp-admin/includes/file.php - - - message: '#^Variable \$_POST in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable - count: 1 - path: ../../../src/wp-admin/includes/media.php - message: '#^Variable \$namespace in isset\(\) always exists and is not nullable\.$#' identifier: isset.variable From c231c30de064ca8a0411c1f9d48499cbe7242a70 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Aug 2026 13:54:25 -0700 Subject: [PATCH 6/8] Remove redundant isset( $namespace ) check in WP_Block_Parser::next_token() --- src/wp-includes/class-wp-block-parser.php | 2 +- tests/phpstan/baselines/isset.variable.neon | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-block-parser.php b/src/wp-includes/class-wp-block-parser.php index ea66e3b51d38d..02b40dfdddc78 100644 --- a/src/wp-includes/class-wp-block-parser.php +++ b/src/wp-includes/class-wp-block-parser.php @@ -268,7 +268,7 @@ public function next_token() { $is_closer = isset( $matches['closer'] ) && -1 !== $matches['closer'][1]; $is_void = isset( $matches['void'] ) && -1 !== $matches['void'][1]; $namespace = $matches['namespace']; - $namespace = ( isset( $namespace ) && -1 !== $namespace[1] ) ? $namespace[0] : 'core/'; + $namespace = -1 !== $namespace[1] ? $namespace[0] : 'core/'; $name = $namespace . $matches['name'][0]; $has_attrs = isset( $matches['attrs'] ) && -1 !== $matches['attrs'][1]; diff --git a/tests/phpstan/baselines/isset.variable.neon b/tests/phpstan/baselines/isset.variable.neon index 9e64459b360f8..9ced181cf2421 100644 --- a/tests/phpstan/baselines/isset.variable.neon +++ b/tests/phpstan/baselines/isset.variable.neon @@ -23,8 +23,3 @@ parameters: identifier: isset.variable count: 1 path: ../../../src/wp-admin/includes/file.php - - - message: '#^Variable \$namespace in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable - count: 1 - path: ../../../src/wp-includes/class-wp-block-parser.php From e84eacdecd116bdcd58ec408286715b952b1f215 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Aug 2026 13:58:01 -0700 Subject: [PATCH 7/8] Fix last isset.variable error in wp_edit_theme_plugin_file() --- phpstan.neon.dist | 1 - src/wp-admin/includes/file.php | 9 ++++---- tests/phpstan/baselines/isset.variable.neon | 25 --------------------- 3 files changed, 5 insertions(+), 30 deletions(-) delete mode 100644 tests/phpstan/baselines/isset.variable.neon diff --git a/phpstan.neon.dist b/phpstan.neon.dist index fa1e3f209c322..49a0412cb6a57 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -57,7 +57,6 @@ includes: - tests/phpstan/baselines/instanceof.alwaysTrue.neon - tests/phpstan/baselines/isset.offset.neon - tests/phpstan/baselines/isset.property.neon - - tests/phpstan/baselines/isset.variable.neon - tests/phpstan/baselines/method.childParameterType.neon - tests/phpstan/baselines/method.nonObject.neon - tests/phpstan/baselines/method.notFound.neon diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 8c0015020f35d..99b9735011bbc 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -397,9 +397,10 @@ function wp_edit_theme_plugin_file( $args ) { $file = $args['file']; $content = $args['newcontent']; - $plugin = null; - $theme = null; - $real_file = null; + $plugin = null; + $stylesheet = null; + $theme = null; + $real_file = null; if ( ! empty( $args['plugin'] ) ) { $plugin = $args['plugin']; @@ -560,7 +561,7 @@ function wp_edit_theme_plugin_file( $args ) { // Attempt loopback request to editor to see if user just whitescreened themselves. if ( $plugin ) { $url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) ); - } elseif ( isset( $stylesheet ) ) { + } elseif ( $stylesheet ) { $url = add_query_arg( array( 'theme' => $stylesheet, diff --git a/tests/phpstan/baselines/isset.variable.neon b/tests/phpstan/baselines/isset.variable.neon deleted file mode 100644 index 9ced181cf2421..0000000000000 --- a/tests/phpstan/baselines/isset.variable.neon +++ /dev/null @@ -1,25 +0,0 @@ -# PHPStan baseline for the `isset.variable` errors in WordPress core. -# -# https://phpstan.org/error-identifiers/isset.variable -# -# Each entry is scoped to a single file and carries an exact occurrence count, -# so that a new instance is reported as a new error rather than being absorbed -# silently. Fixing an occurrence therefore means decrementing or removing its -# entry here as part of the same change. -# -# The goal is to empty this file and delete it, along with the `includes` entry -# for it in phpstan.neon.dist. -# -# Generated by `composer phpstan:baselines`. Do not edit by hand; regenerate with -# -# composer phpstan:baselines -- --identifier=isset.variable -# -# which reruns the analysis with this file suppressed so the errors surface again. - -parameters: - ignoreErrors: - - - message: '#^Variable \$stylesheet in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable - count: 1 - path: ../../../src/wp-admin/includes/file.php From 99f51e439ee4663043ef582db89ecb389833f4a5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 12 Aug 2026 14:46:34 -0700 Subject: [PATCH 8/8] Scope the query_vars shape to load_template() and type 's' accurately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `@phpstan-var array{ s?: string, ... }` on the public `WP_Query::$query_vars` property was both inaccurate and net-negative, so move it to a local annotation inside the one function that needs it. It was inaccurate because `parse_query()` gates `s` only with `is_scalar()`, so ints, floats and bools pass through untouched. That is deliberate, documented behavior: `Tests_Query_ParseQuery::test_parse_query_s_type` asserts that `3`, `3.5` and `true` all survive a round trip unchanged. It was net-negative because an unsealed array shape is stricter than a plain `array` for offset reads — every key other than `s` becomes "might not exist". Narrowing the shared property removed 4 errors and introduced 12 more at rule level 10, spread across `WP_Query` itself, `WP_Media_List_Table` and three REST controllers, plus a further 12 in the `parseQuery` test file. None of those files changed, so a diff-of-changed-lines check could not have caught them. Annotating the local `$query_vars` instead confines the narrowing to `load_template()`, where it is the only thing `extract()` has to work from. Typing `s` as `scalar` rather than `string` is the honest type, which in turn makes the cast in the `esc_attr()` call necessary; that cast is behavior-preserving, since `esc_attr()` already coerces its argument. A plain `@var` tag is used rather than `@phpstan-var` so that IDEs read it too. Documenting the full shape of `query_vars` is left to Core-60745. Co-Authored-By: Claude Opus 5 --- src/wp-includes/class-wp-query.php | 4 ---- src/wp-includes/template.php | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index dd7acecf677d1..228691d26d12b 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -31,10 +31,6 @@ class WP_Query { * * @since 1.5.0 * @var array - * @phpstan-var array{ - * s?: string, - * ... - * } */ public $query_vars = array(); diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index eaa665c2f2d99..d2b74e188c095 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -782,6 +782,7 @@ function locate_template( $template_names, $load = false, $load_once = true, $ar function load_template( $_template_file, $load_once = true, $args = array() ) { global $posts, $post, $wp_did_header, $wp_query, $wp_rewrite, $wpdb, $wp_version, $wp, $id, $comment, $user_ID; + /** @var array{ s?: scalar, ... } $query_vars */ $query_vars = $wp_query->query_vars; if ( is_array( $query_vars ) ) { /* @@ -797,7 +798,7 @@ function load_template( $_template_file, $load_once = true, $args = array() ) { } if ( isset( $s ) ) { - $s = esc_attr( $s ); + $s = esc_attr( (string) $s ); } /**