From 6d835969801f10d2564b715cb2078f4580027763 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 24 Feb 2026 21:48:31 -0800 Subject: [PATCH 01/51] Ignore errors in src/wp-includes/build which currently come the Gutenberg build --- tests/phpstan/base.neon | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 347c2198ae953..8260f02b3aa88 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -105,6 +105,8 @@ parameters: - ../../src/wp-includes/deprecated.php - ../../src/wp-includes/ms-deprecated.php - ../../src/wp-includes/pluggable-deprecated.php + # These files are currently pulled in from Gutenberg, but maybe not long-term. See . + - ../../src/wp-includes/build # These files are sourced by wordpress/gutenberg in `tools/release/sync-stable-blocks.js`. - ../../src/wp-includes/blocks # Third-party libraries. From 5dec036584b5d7811c139d74fc542dfe845c8876 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 24 Feb 2026 22:37:27 -0800 Subject: [PATCH 02/51] Bump phpstan to 2.1.40 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 17f53c2116f71..649a3d429324c 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "squizlabs/php_codesniffer": "3.13.5", "wp-coding-standards/wpcs": "~3.3.0", "phpcompatibility/phpcompatibility-wp": "~2.1.3", - "phpstan/phpstan": "2.1.39", + "phpstan/phpstan": "2.1.40", "yoast/phpunit-polyfills": "^1.1.0" }, "config": { From 593a10ca6bf9b9fe148dc56eb6b5e72fe9fcbe18 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 18:07:14 -0800 Subject: [PATCH 03/51] Bump PHPStan to level 1 --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index e74e6ec1a441b..186eac7f59505 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -14,7 +14,7 @@ includes: parameters: # https://phpstan.org/user-guide/rule-levels - level: 0 + level: 1 reportUnmatchedIgnoredErrors: true ignoreErrors: From 22de4b6588239441a9d0852b67820cda399af2f8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 18:09:21 -0800 Subject: [PATCH 04/51] Ignore variable.undefined errors since too noisy Co-authored-by: Dovid Levine --- phpstan.neon.dist | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 186eac7f59505..38dd9c63098d0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -34,3 +34,8 @@ parameters: identifier: function.inner path: src/wp-includes/canonical.php count: 1 + + # Level 1: + - # These are too noisy at the moment. + identifier: variable.undefined + message: '#Variable \$[a-zA-Z0-9_]+ might not be defined\.#' From cb8db153dddbb84bb7dc3920e5aa7c12b0154946 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 18:10:20 -0800 Subject: [PATCH 05/51] Regenerate baseline for level 1 --- tests/phpstan/baseline.php | 190 ++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index 646cbdbef630c..d0443d8bbf7b1 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -1,3 +1,191 @@ '#^Call to function compact\\(\\) contains possibly undefined variable \\$comment_author\\.$#', + 'identifier' => 'variable.undefined', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/includes/ajax-actions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Call to function compact\\(\\) contains possibly undefined variable \\$comment_author_email\\.$#', + 'identifier' => 'variable.undefined', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/includes/ajax-actions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Call to function compact\\(\\) contains possibly undefined variable \\$comment_author_url\\.$#', + 'identifier' => 'variable.undefined', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/includes/ajax-actions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Call to function compact\\(\\) contains possibly undefined variable \\$user_id\\.$#', + 'identifier' => 'variable.undefined', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/includes/ajax-actions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$_POST in isset\\(\\) always exists and is not nullable\\.$#', + 'identifier' => 'isset.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/includes/class-custom-image-header.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constructor of class WP_Filesystem_Direct has an unused parameter \\$arg\\.$#', + 'identifier' => 'constructor.unusedParameter', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/includes/class-wp-filesystem-direct.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$class in empty\\(\\) always exists and is always falsy\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/includes/class-wp-posts-list-table.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$_POST in isset\\(\\) always exists and is not nullable\\.$#', + 'identifier' => 'isset.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/includes/media.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$parent_file in empty\\(\\) always exists and is not falsy\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-admin/themes.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constant HEADER_IMAGE_HEIGHT not found\\.$#', + 'identifier' => 'constant.notFound', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/functions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', + 'identifier' => 'constant.notFound', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/functions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constant HEADER_TEXTCOLOR not found\\.$#', + 'identifier' => 'constant.notFound', + 'count' => 2, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/functions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', + 'identifier' => 'constant.notFound', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/header.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', + 'identifier' => 'constant.notFound', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/showcase.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', + 'identifier' => 'arguments.count', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', + 'identifier' => 'arguments.count', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', + 'identifier' => 'arguments.count', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constant HEADER_IMAGE_HEIGHT not found\\.$#', + 'identifier' => 'constant.notFound', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyten/functions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', + 'identifier' => 'constant.notFound', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyten/functions.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', + 'identifier' => 'constant.notFound', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyten/header.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Function wp_cache_switch_to_blog_fallback invoked with 1 parameter, 0 required\\.$#', + 'identifier' => 'arguments.count', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/cache-compat.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$addl_path in empty\\(\\) always exists and is always falsy\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/canonical.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$namespace in isset\\(\\) always exists and is not nullable\\.$#', + 'identifier' => 'isset.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/class-wp-block-parser.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$block_type in empty\\(\\) always exists and is not falsy\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/class-wp-block-supports.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$loader in isset\\(\\) always exists and is not nullable\\.$#', + 'identifier' => 'isset.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/class-wp-oembed.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$search in empty\\(\\) always exists and is not falsy\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/class-wp-query.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$status_type_clauses in empty\\(\\) always exists and is not falsy\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/class-wp-query.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$deprecated in empty\\(\\) always exists and is always falsy\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/pluggable.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$schema in empty\\(\\) is never defined\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$the_parent in empty\\(\\) always exists and is not falsy\\.$#', + 'identifier' => 'empty.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/taxonomy.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Variable \\$s in isset\\(\\) is never defined\\.$#', + 'identifier' => 'isset.variable', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-includes/template.php', +]; + +return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; From d3dd8ecd6306dabce1c12cccb94044f0595fe22e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 19:08:39 -0800 Subject: [PATCH 06/51] Remove unused parameter for WP_Filesystem_Direct constructor --- src/wp-admin/includes/class-wp-filesystem-direct.php | 4 +--- tests/phpstan/baseline.php | 6 ------ 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/wp-admin/includes/class-wp-filesystem-direct.php b/src/wp-admin/includes/class-wp-filesystem-direct.php index a4b197c15229f..34862a661d321 100644 --- a/src/wp-admin/includes/class-wp-filesystem-direct.php +++ b/src/wp-admin/includes/class-wp-filesystem-direct.php @@ -19,10 +19,8 @@ class WP_Filesystem_Direct extends WP_Filesystem_Base { * Constructor. * * @since 2.5.0 - * - * @param mixed $arg Not used. */ - public function __construct( $arg ) { + public function __construct() { $this->method = 'direct'; $this->errors = new WP_Error(); } diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index d0443d8bbf7b1..d656589f4a069 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -31,12 +31,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/wp-admin/includes/class-custom-image-header.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Constructor of class WP_Filesystem_Direct has an unused parameter \\$arg\\.$#', - 'identifier' => 'constructor.unusedParameter', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/includes/class-wp-filesystem-direct.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$class in empty\\(\\) always exists and is always falsy\\.$#', 'identifier' => 'empty.variable', From bf92684d1a5041de6a67abd1ce7b3ce38dcf3b05 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 19:17:57 -0800 Subject: [PATCH 07/51] Add theme constants to config to address constant.notFound errors Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/phpstan/base.neon | 7 ++++++ tests/phpstan/baseline.php | 48 ------------------------------------- tests/phpstan/bootstrap.php | 9 +++++++ 3 files changed, 16 insertions(+), 48 deletions(-) diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 8260f02b3aa88..7bfe9d6eff3df 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -25,6 +25,8 @@ parameters: - ALLOW_SUBDIRECTORY_INSTALL - AUTH_SALT - AUTOMATIC_UPDATER_DISABLED + - BACKGROUND_COLOR + - BACKGROUND_IMAGE - COOKIEPATH - CUSTOM_TAGS - DISALLOW_FILE_EDIT @@ -32,8 +34,13 @@ parameters: - EMPTY_TRASH_DAYS - ENFORCE_GZIP - FORCE_SSL_LOGIN + - HEADER_IMAGE + - HEADER_IMAGE_HEIGHT + - HEADER_IMAGE_WIDTH + - HEADER_TEXTCOLOR - MEDIA_TRASH - MULTISITE + - NO_HEADER_TEXT - NOBLOGREDIRECT - SAVEQUERIES - SCRIPT_DEBUG diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index d656589f4a069..2ad8bac653169 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -49,36 +49,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/wp-admin/themes.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Constant HEADER_IMAGE_HEIGHT not found\\.$#', - 'identifier' => 'constant.notFound', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/functions.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', - 'identifier' => 'constant.notFound', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/functions.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Constant HEADER_TEXTCOLOR not found\\.$#', - 'identifier' => 'constant.notFound', - 'count' => 2, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/functions.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', - 'identifier' => 'constant.notFound', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/header.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', - 'identifier' => 'constant.notFound', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyeleven/showcase.php', -]; $ignoreErrors[] = [ 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', 'identifier' => 'arguments.count', @@ -97,24 +67,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Constant HEADER_IMAGE_HEIGHT not found\\.$#', - 'identifier' => 'constant.notFound', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyten/functions.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', - 'identifier' => 'constant.notFound', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyten/functions.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Constant HEADER_IMAGE_WIDTH not found\\.$#', - 'identifier' => 'constant.notFound', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyten/header.php', -]; $ignoreErrors[] = [ 'message' => '#^Function wp_cache_switch_to_blog_fallback invoked with 1 parameter, 0 required\\.$#', 'identifier' => 'arguments.count', diff --git a/tests/phpstan/bootstrap.php b/tests/phpstan/bootstrap.php index c87a26babf83d..77a597609270e 100644 --- a/tests/phpstan/bootstrap.php +++ b/tests/phpstan/bootstrap.php @@ -93,3 +93,12 @@ define( 'FS_TIMEOUT', 1 ); define( 'FS_CHMOD_DIR', 1 ); define( 'FS_CHMOD_FILE', 1 ); + +// Theme constants referenced in add_theme_support(). +define( 'NO_HEADER_TEXT', false ); +define( 'HEADER_IMAGE_WIDTH', 0 ); +define( 'HEADER_IMAGE_HEIGHT', 0 ); +define( 'HEADER_TEXTCOLOR', '' ); +define( 'HEADER_IMAGE', '' ); +define( 'BACKGROUND_COLOR', '' ); +define( 'BACKGROUND_IMAGE', '' ); From d39600aa5b0d650b6d49cb6ce23594e3dfd95772 Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Tue, 24 Feb 2026 09:18:58 +0100 Subject: [PATCH 08/51] Docs: Add `@return never` to `dead_db()`. --- src/wp-includes/functions.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 4b6330a697780..e4b96c6418c63 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5501,6 +5501,8 @@ function wp_ob_end_flush_all() { * @since 2.3.2 * * @global wpdb $wpdb WordPress database abstraction object. + * + * @return never */ function dead_db() { global $wpdb; From 75797b7afa805a425e77e2b4ac7e6fce3f200c4c Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Tue, 24 Feb 2026 10:42:28 +0100 Subject: [PATCH 09/51] fix(phpstan): Ignore return.never on dead_db() PHPStan cannot verify that wp_die() always terminates due to its conditional return type and treatPhpDocTypesAsCertain being disabled. --- src/wp-includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index e4b96c6418c63..101585166bd65 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5521,7 +5521,7 @@ function dead_db() { } // Otherwise, be terse. - wp_die( '

' . __( 'Error establishing a database connection' ) . '

', __( 'Database Error' ) ); + wp_die( '

' . __( 'Error establishing a database connection' ) . '

', __( 'Database Error' ) ); // @phpstan-ignore return.never (wp_die() always exits by default.) } /** From e55878c43fe22cdd2289b1480e9af195f002101d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 24 Feb 2026 15:18:55 -0800 Subject: [PATCH 10/51] Fix phpstan-return syntax Co-authored-by: Christoph Daum --- src/wp-includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 101585166bd65..afc421079772a 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -3766,7 +3766,7 @@ function wp_nonce_ays( $action ) { * } * @return never|void Returns void if `$args['exit']` is false, otherwise exits. * - * @phpstan-return ( $args['exit'] is false ? void : never ) + * @phpstan-return ( $args is array{exit: false} ? void : never ) */ function wp_die( $message = '', $title = '', $args = array() ) { global $wp_query; From 46b656acb44e755085cb5e7d450b7b882e6a0eda Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 24 Feb 2026 15:19:03 -0800 Subject: [PATCH 11/51] Remove now-unnecessary phpstan-ignore --- src/wp-includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index afc421079772a..8bf46ef0b871f 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -5521,7 +5521,7 @@ function dead_db() { } // Otherwise, be terse. - wp_die( '

' . __( 'Error establishing a database connection' ) . '

', __( 'Database Error' ) ); // @phpstan-ignore return.never (wp_die() always exits by default.) + wp_die( '

' . __( 'Error establishing a database connection' ) . '

', __( 'Database Error' ) ); } /** From 3c46095d92b4321a091937681e4586f9e7f7f9eb Mon Sep 17 00:00:00 2001 From: Christoph Daum Date: Wed, 25 Feb 2026 08:14:38 +0100 Subject: [PATCH 12/51] docs: Refine phpdoc of dead_db() Co-authored-by: Weston Ruter --- src/wp-includes/class-wpdb.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index 23c865b87d817..de4f86ce753b0 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -2117,7 +2117,9 @@ public function parse_db_host( $host ) { * @since 3.9.0 * * @param bool $allow_bail Optional. Allows the function to bail. Default true. - * @return bool|void True if the connection is up. + * @return bool|never Whether the connection is up. + * + * @phpstan-return ( $allow_bail is true ? bool|never : bool ) */ public function check_connection( $allow_bail = true ) { // Check if the connection is alive. From c52438ac35ca5627d07b63fea65c6e5f008fc873 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 19:28:22 -0800 Subject: [PATCH 13/51] Remove never from check_connection return --- src/wp-includes/class-wpdb.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index de4f86ce753b0..60026dbdc5c16 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -2117,7 +2117,7 @@ public function parse_db_host( $host ) { * @since 3.9.0 * * @param bool $allow_bail Optional. Allows the function to bail. Default true. - * @return bool|never Whether the connection is up. + * @return bool Whether the connection is up. Exits when `$allow_bail` is true and the connection is down. * * @phpstan-return ( $allow_bail is true ? bool|never : bool ) */ From 87a2c049b80664d5cfbf3b777b72a13059c49fff Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 19:29:22 -0800 Subject: [PATCH 14/51] Remove errors from baseline which were fixed by never returns --- tests/phpstan/baseline.php | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index 2ad8bac653169..0ebe3bbad7266 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -1,30 +1,6 @@ '#^Call to function compact\\(\\) contains possibly undefined variable \\$comment_author\\.$#', - 'identifier' => 'variable.undefined', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/includes/ajax-actions.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to function compact\\(\\) contains possibly undefined variable \\$comment_author_email\\.$#', - 'identifier' => 'variable.undefined', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/includes/ajax-actions.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to function compact\\(\\) contains possibly undefined variable \\$comment_author_url\\.$#', - 'identifier' => 'variable.undefined', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/includes/ajax-actions.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Call to function compact\\(\\) contains possibly undefined variable \\$user_id\\.$#', - 'identifier' => 'variable.undefined', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/includes/ajax-actions.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$_POST in isset\\(\\) always exists and is not nullable\\.$#', 'identifier' => 'isset.variable', From 95ddce21f75478d4b34e7873315e0de7b4c39885 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Mar 2026 19:37:31 -0800 Subject: [PATCH 15/51] Resolve isset.variable error since $_POST global is always set --- src/wp-admin/includes/class-custom-image-header.php | 2 +- tests/phpstan/baseline.php | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/wp-admin/includes/class-custom-image-header.php b/src/wp-admin/includes/class-custom-image-header.php index eb65605728062..2c0eb26c0fc9a 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/baseline.php b/tests/phpstan/baseline.php index 0ebe3bbad7266..810d719f57b6a 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -1,12 +1,6 @@ '#^Variable \\$_POST in isset\\(\\) always exists and is not nullable\\.$#', - 'identifier' => 'isset.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/includes/class-custom-image-header.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$class in empty\\(\\) always exists and is always falsy\\.$#', 'identifier' => 'empty.variable', From 2d774486763c0297208fb104a3c363b749eb2e5d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 11:43:06 -0800 Subject: [PATCH 16/51] Use allow-list for unknown globals --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 38dd9c63098d0..1a0e1f5dd2f01 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -38,4 +38,4 @@ parameters: # Level 1: - # These are too noisy at the moment. identifier: variable.undefined - message: '#Variable \$[a-zA-Z0-9_]+ might not be defined\.#' + message: '#Variable \$(_comments|_nav_menu_selected_id|_object|_wp_admin_css_colors|all_items|authors_dropdown|auto_update_notice|auto_updates|b|badge_class|badge_title|blog_id|cache_found|cache_key|cached|callback|cat_id|cats|class|closer_potentially_starts_at|comment|comment_closing_at|comment_id|comment_ids|comment_opening_at|comment_status|comments|compat|context|control|control_callback|current_blog|current_screen|current_site|current_user|description|docs_select|end|error|filter_id|gen|h1|has_closer|has_void_flag|height|html|icon|id|image|is_IE|is_caddy|is_nginx|item_name|json_at|json_length|k|key|link|linkcheck|manage_url|matches|message|messages|metakey|mime|my_year|name|name_at|name_length|namespace_at|network|network_current|next_id|node|num_posts|old_user_data|option|options_to_update|original_content|original_result|output|page|paged|parent|parts|plugin_name|plugins|post|post_author|post_meta|post_type_cap|posts|primary|processed_response|query|redirect|required_mysql_version|required_php_version|resized|response|restore_link|result|return_to_post|revision|rollback_result|rootcommentmatch|rootcommentquery|rp_key|rp_login|s|secret_keys|section_id|self|shape|show_network_active|stat|status|sub1|sub1comment|sub1embed|sub1feed|sub1feed2|sub1tb|sub2|sub2comment|sub2embed|sub2feed|sub2feed2|sub2tb|subcommentquery|subembedquery|subfeedquery|subject|submenu|subquery|subtbquery|tab|table_prefix|tag|tag_ID|tax|taxnow|taxonomy|text|title|token_length|trackbackmatch|trackbackquery|type|typenow|unique_id|update|update_data|upgrader|user_ID|user_email|user_id|user_login|usersearch|version_url|was_active|width|working_dir_local|wp_db_version|wp_filter|wp_query|wp_registered_sidebars|wp_registered_widget_controls|wp_registered_widget_updates|wp_rewrite|wp_roles|wp_version|wpdb)\b might not be defined\.#' From eeefc04b560ba4dc45c1434cc7cea29a8d59be33 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 11:50:01 -0800 Subject: [PATCH 17/51] Convert single variable.undefined ignore rule into one per variable --- phpstan.neon.dist | 520 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 518 insertions(+), 2 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1a0e1f5dd2f01..8364f71cca9f5 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -36,6 +36,522 @@ parameters: count: 1 # Level 1: - - # These are too noisy at the moment. + - + identifier: variable.undefined + message: '#Variable \$_comments might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$_nav_menu_selected_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$_object might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$_wp_admin_css_colors might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$all_items might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$authors_dropdown might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$auto_update_notice might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$auto_updates might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$b might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$badge_class might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$badge_title might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$blog_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$cache_found might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$cache_key might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$cached might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$callback might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$cat_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$cats might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$class might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$closer_potentially_starts_at might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$comment might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$comment_closing_at might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$comment_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$comment_ids might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$comment_opening_at might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$comment_status might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$comments might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$compat might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$context might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$control might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$control_callback might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$current_blog might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$current_screen might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$current_site might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$current_user might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$description might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$docs_select might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$end might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$error might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$filter_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$gen might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$h1 might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$has_closer might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$has_void_flag might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$height might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$html might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$icon might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$image might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$is_IE might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$is_caddy might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$is_nginx might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$item_name might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$json_at might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$json_length might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$k might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$key might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$link might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$linkcheck might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$manage_url might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$matches might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$message might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$messages might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$metakey might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$mime might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$my_year might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$name might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$name_at might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$name_length might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$namespace_at might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$network might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$network_current might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$next_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$node might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$num_posts might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$old_user_data might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$option might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$options_to_update might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$original_content might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$original_result might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$output might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$page might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$paged might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$parent might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$parts might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$plugin_name might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$plugins might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$post might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$post_author might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$post_meta might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$post_type_cap might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$posts might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$primary might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$processed_response might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$query might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$redirect might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$required_mysql_version might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$required_php_version might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$resized might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$response might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$restore_link might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$result might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$return_to_post might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$revision might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$rollback_result might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$rootcommentmatch might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$rootcommentquery might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$rp_key might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$rp_login might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$s might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$secret_keys might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$section_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$self might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$shape might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$show_network_active might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$stat might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$status might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub1 might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub1comment might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub1embed might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub1feed might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub1feed2 might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub1tb might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub2 might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub2comment might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub2embed might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub2feed might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub2feed2 might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$sub2tb might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$subcommentquery might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$subembedquery might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$subfeedquery might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$subject might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$submenu might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$subquery might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$subtbquery might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$tab might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$table_prefix might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$tag might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$tag_ID might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$tax might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$taxnow might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$taxonomy might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$text might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$title might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$token_length might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$trackbackmatch might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$trackbackquery might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$type might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$typenow might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$unique_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$update might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$update_data might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$upgrader might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$user_ID might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$user_email might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$user_id might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$user_login might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$usersearch might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$version_url might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$was_active might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$width might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$working_dir_local might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_db_version might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_filter might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_query might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_registered_sidebars might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_registered_widget_controls might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_registered_widget_updates might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_rewrite might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_roles might not be defined\.#' + - + identifier: variable.undefined + message: '#Variable \$wp_version might not be defined\.#' + - identifier: variable.undefined - message: '#Variable \$(_comments|_nav_menu_selected_id|_object|_wp_admin_css_colors|all_items|authors_dropdown|auto_update_notice|auto_updates|b|badge_class|badge_title|blog_id|cache_found|cache_key|cached|callback|cat_id|cats|class|closer_potentially_starts_at|comment|comment_closing_at|comment_id|comment_ids|comment_opening_at|comment_status|comments|compat|context|control|control_callback|current_blog|current_screen|current_site|current_user|description|docs_select|end|error|filter_id|gen|h1|has_closer|has_void_flag|height|html|icon|id|image|is_IE|is_caddy|is_nginx|item_name|json_at|json_length|k|key|link|linkcheck|manage_url|matches|message|messages|metakey|mime|my_year|name|name_at|name_length|namespace_at|network|network_current|next_id|node|num_posts|old_user_data|option|options_to_update|original_content|original_result|output|page|paged|parent|parts|plugin_name|plugins|post|post_author|post_meta|post_type_cap|posts|primary|processed_response|query|redirect|required_mysql_version|required_php_version|resized|response|restore_link|result|return_to_post|revision|rollback_result|rootcommentmatch|rootcommentquery|rp_key|rp_login|s|secret_keys|section_id|self|shape|show_network_active|stat|status|sub1|sub1comment|sub1embed|sub1feed|sub1feed2|sub1tb|sub2|sub2comment|sub2embed|sub2feed|sub2feed2|sub2tb|subcommentquery|subembedquery|subfeedquery|subject|submenu|subquery|subtbquery|tab|table_prefix|tag|tag_ID|tax|taxnow|taxonomy|text|title|token_length|trackbackmatch|trackbackquery|type|typenow|unique_id|update|update_data|upgrader|user_ID|user_email|user_id|user_login|usersearch|version_url|was_active|width|working_dir_local|wp_db_version|wp_filter|wp_query|wp_registered_sidebars|wp_registered_widget_controls|wp_registered_widget_updates|wp_rewrite|wp_roles|wp_version|wpdb)\b might not be defined\.#' + message: '#Variable \$wpdb might not be defined\.#' From cb7a8c3c0308e052848ffc97a5e641da12721b04 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 12:46:00 -0800 Subject: [PATCH 18/51] Remove unused $class variable after r54215 (56bde67811f432764d2be6c5c830c634624e7081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes PHPStan issue: ------ --------------------------------------------------------------- Line wp-admin/includes/class-wp-posts-list-table.php ------ --------------------------------------------------------------- 357 Variable $class in empty() always exists and is always falsy. 🪪 empty.variable at src/wp-admin/includes/class-wp-posts-list-table.php:357 ------ --------------------------------------------------------------- --- .../includes/class-wp-posts-list-table.php | 16 +--------------- tests/phpstan/baseline.php | 6 ------ 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index f9c08ad5c73ee..ae444905ac79f 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -298,7 +298,6 @@ protected function get_views() { $status_links = array(); $num_posts = wp_count_posts( $post_type, 'readable' ); $total_posts = array_sum( (array) $num_posts ); - $class = ''; $current_user_id = get_current_user_id(); $all_args = array( 'post_type' => $post_type ); @@ -310,10 +309,6 @@ protected function get_views() { } if ( $this->user_posts_count && $this->user_posts_count !== $total_posts ) { - if ( isset( $_GET['author'] ) && ( $current_user_id === (int) $_GET['author'] ) ) { - $class = 'current'; - } - $mine_args = array( 'post_type' => $post_type, 'author' => $current_user_id, @@ -337,7 +332,6 @@ protected function get_views() { ); $all_args['all_posts'] = 1; - $class = ''; } $all_inner_html = sprintf( @@ -354,7 +348,7 @@ protected function get_views() { $status_links['all'] = array( 'url' => esc_url( add_query_arg( $all_args, 'edit.php' ) ), 'label' => $all_inner_html, - 'current' => empty( $class ) && ( $this->is_base_request() || isset( $_REQUEST['all_posts'] ) ), + 'current' => ( $this->is_base_request() || isset( $_REQUEST['all_posts'] ) ), ); if ( $mine ) { @@ -362,18 +356,12 @@ protected function get_views() { } foreach ( get_post_stati( array( 'show_in_admin_status_list' => true ), 'objects' ) as $status ) { - $class = ''; - $status_name = $status->name; if ( ! in_array( $status_name, $avail_post_stati, true ) || empty( $num_posts->$status_name ) ) { continue; } - if ( isset( $_REQUEST['post_status'] ) && $status_name === $_REQUEST['post_status'] ) { - $class = 'current'; - } - $status_args = array( 'post_status' => $status_name, 'post_type' => $post_type, @@ -392,8 +380,6 @@ protected function get_views() { } if ( ! empty( $this->sticky_posts_count ) ) { - $class = ! empty( $_REQUEST['show_sticky'] ) ? 'current' : ''; - $sticky_args = array( 'post_type' => $post_type, 'show_sticky' => 1, diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index 810d719f57b6a..89265768f863a 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -1,12 +1,6 @@ '#^Variable \\$class in empty\\(\\) always exists and is always falsy\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/includes/class-wp-posts-list-table.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$_POST in isset\\(\\) always exists and is not nullable\\.$#', 'identifier' => 'isset.variable', From a47a4344cfcc6428b8ecf33080bdbdc0058441c9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 12:51:21 -0800 Subject: [PATCH 19/51] Fix wp_cache_switch_to_blog() issues --- src/wp-includes/cache-compat.php | 6 +++--- tests/phpstan/baseline.php | 6 ------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/cache-compat.php b/src/wp-includes/cache-compat.php index fe0e29c107b64..6f8842ab04ae0 100644 --- a/src/wp-includes/cache-compat.php +++ b/src/wp-includes/cache-compat.php @@ -327,11 +327,11 @@ function wp_cache_set_multiple_salted( $data, $group, $salt, $expire = 0 ) { * * @param int $blog_id Site ID. */ - function wp_cache_switch_to_blog( $blog_id ) { + function wp_cache_switch_to_blog( $blog_id ): void { global $wp_object_cache; // Attempt to use the drop-in object cache method if it exists. - if ( method_exists( $wp_object_cache, 'switch_to_blog' ) ) { + if ( is_object( $wp_object_cache ) && method_exists( $wp_object_cache, 'switch_to_blog' ) ) { $wp_object_cache->switch_to_blog( $blog_id ); return; } @@ -340,6 +340,6 @@ function wp_cache_switch_to_blog( $blog_id ) { * Perform a fallback blog switch, which will reinitialize the caches * for the new blog ID. */ - wp_cache_switch_to_blog_fallback( $blog_id ); + wp_cache_switch_to_blog_fallback(); } endif; diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index 89265768f863a..24b1c4aac44a8 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -31,12 +31,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Function wp_cache_switch_to_blog_fallback invoked with 1 parameter, 0 required\\.$#', - 'identifier' => 'arguments.count', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/cache-compat.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$addl_path in empty\\(\\) always exists and is always falsy\\.$#', 'identifier' => 'empty.variable', From 680d165e5fc662f16ea2530cdf302053119f4c40 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 12:54:18 -0800 Subject: [PATCH 20/51] Fix arguments.count issues in Twenty Seventeen where twentyseventeen_edit_link() takes no args --- .../page/content-front-page-panels.php | 2 +- .../template-parts/page/content-front-page.php | 2 +- .../template-parts/page/content-page.php | 2 +- tests/phpstan/baseline.php | 18 ------------------ 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php b/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php index 923c11f6132a1..f6f009a63a1c6 100644 --- a/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php +++ b/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php @@ -33,7 +33,7 @@
', '' ); ?> - +
diff --git a/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php b/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php index b80749861e291..0cea464c4c6b9 100644 --- a/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php +++ b/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php @@ -30,7 +30,7 @@
', '' ); ?> - +
diff --git a/src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php b/src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php index 6e99666e8dbee..f34d5fc829892 100644 --- a/src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php +++ b/src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php @@ -15,7 +15,7 @@
>
', '' ); ?> - +
1, 'path' => __DIR__ . '/../../src/wp-admin/themes.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', - 'identifier' => 'arguments.count', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', - 'identifier' => 'arguments.count', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', - 'identifier' => 'arguments.count', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$addl_path in empty\\(\\) always exists and is always falsy\\.$#', 'identifier' => 'empty.variable', From a08886787c5a9d7506b422085e462f80bb987e36 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 18:05:42 -0800 Subject: [PATCH 21/51] Remove $comment_id variable.undefined error after r61859 (ce40cd36) --- phpstan.neon.dist | 3 --- 1 file changed, 3 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 8364f71cca9f5..c433a89457cc1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -102,9 +102,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$comment_closing_at might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$comment_id might not be defined\.#' - identifier: variable.undefined message: '#Variable \$comment_ids might not be defined\.#' From 202c39cbb02847eb6264179f1336f5ed0bc2ff32 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 18:13:29 -0800 Subject: [PATCH 22/51] Remove $shape from being a variable.undefined ignored error --- phpstan.neon.dist | 3 --- 1 file changed, 3 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index c433a89457cc1..10864210817df 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -372,9 +372,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$self might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$shape might not be defined\.#' - identifier: variable.undefined message: '#Variable \$show_network_active might not be defined\.#' From de684c6ae6c9856de061244c3a9cd5854db5708d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 19:50:28 -0800 Subject: [PATCH 23/51] Revert "Fix arguments.count issues in Twenty Seventeen where twentyseventeen_edit_link() takes no args" This reverts commit 680d165e5fc662f16ea2530cdf302053119f4c40. --- .../page/content-front-page-panels.php | 2 +- .../template-parts/page/content-front-page.php | 2 +- .../template-parts/page/content-page.php | 2 +- tests/phpstan/baseline.php | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php b/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php index f6f009a63a1c6..923c11f6132a1 100644 --- a/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php +++ b/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php @@ -33,7 +33,7 @@
', '' ); ?> - +
diff --git a/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php b/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php index 0cea464c4c6b9..b80749861e291 100644 --- a/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php +++ b/src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php @@ -30,7 +30,7 @@
', '' ); ?> - +
diff --git a/src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php b/src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php index f34d5fc829892..6e99666e8dbee 100644 --- a/src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php +++ b/src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php @@ -15,7 +15,7 @@
>
', '' ); ?> - +
1, 'path' => __DIR__ . '/../../src/wp-admin/themes.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', + 'identifier' => 'arguments.count', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', + 'identifier' => 'arguments.count', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', + 'identifier' => 'arguments.count', + 'count' => 1, + 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php', +]; $ignoreErrors[] = [ 'message' => '#^Variable \\$addl_path in empty\\(\\) always exists and is always falsy\\.$#', 'identifier' => 'empty.variable', From db38dbfe953c70d5608dd1a037273a86399cb496 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 20:23:19 -0800 Subject: [PATCH 24/51] Initialize $shape variable to suppress variable.undefined error --- src/wp-includes/theme-compat/embed-content.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/theme-compat/embed-content.php b/src/wp-includes/theme-compat/embed-content.php index 42884fec0c5bc..8bf961ba2dace 100644 --- a/src/wp-includes/theme-compat/embed-content.php +++ b/src/wp-includes/theme-compat/embed-content.php @@ -13,6 +13,7 @@
> Date: Thu, 12 Mar 2026 21:35:57 -0700 Subject: [PATCH 25/51] Remove errors from baseline fixed in r62000 --- tests/phpstan/baseline.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index 24b1c4aac44a8..654affdc27945 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -13,24 +13,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/wp-admin/themes.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', - 'identifier' => 'arguments.count', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', - 'identifier' => 'arguments.count', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Function twentyseventeen_edit_link invoked with 1 parameter, 0 required\\.$#', - 'identifier' => 'arguments.count', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-content/themes/twentyseventeen/template-parts/page/content-page.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$addl_path in empty\\(\\) always exists and is always falsy\\.$#', 'identifier' => 'empty.variable', From e003c668466cc4404d7c8aba2b383d9708d28847 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 12 Mar 2026 21:44:19 -0700 Subject: [PATCH 26/51] Remove empty.variable from baseline which was fixed in r61995 --- tests/phpstan/baseline.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index 654affdc27945..28e56b88545bf 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -49,12 +49,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/wp-includes/class-wp-query.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$deprecated in empty\\(\\) always exists and is always falsy\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/pluggable.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$schema in empty\\(\\) is never defined\\.$#', 'identifier' => 'empty.variable', From 1a7ebe04a3b538a0a3f253f040fbd0ab50c2b74c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 09:33:32 -0700 Subject: [PATCH 27/51] Remove PHPStan ignore entries that no longer match any error Merging trunk brought in fixes for many of the undefined variables this branch had suppressed. With `reportUnmatchedIgnoredErrors: true` those stale entries are themselves reported as errors, so drop them: * 40 `variable.undefined` entries from the Level 1 block in phpstan.neon.dist. * 1 `empty.variable` entry for class-wp-block-supports.php from the baseline. The entries were derived from PHPStan's own "was not matched in reported errors" output rather than by inspection. Re-running afterwards reports no unmatched ignores, and the count of real errors is unchanged at 9, confirming nothing was over-pruned. Co-Authored-By: Claude Opus 5 (1M context) --- phpstan.neon.dist | 120 ------------------------------------- tests/phpstan/baseline.php | 6 -- 2 files changed, 126 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5bb6fe5839374..a6d0bc6803c62 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -45,30 +45,18 @@ parameters: - identifier: variable.undefined message: '#Variable \$_comments might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$_nav_menu_selected_id might not be defined\.#' - identifier: variable.undefined message: '#Variable \$_object might not be defined\.#' - identifier: variable.undefined message: '#Variable \$_wp_admin_css_colors might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$all_items might not be defined\.#' - identifier: variable.undefined message: '#Variable \$authors_dropdown might not be defined\.#' - identifier: variable.undefined message: '#Variable \$auto_update_notice might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$auto_updates might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$b might not be defined\.#' - identifier: variable.undefined message: '#Variable \$badge_class might not be defined\.#' @@ -93,15 +81,9 @@ parameters: - identifier: variable.undefined message: '#Variable \$cat_id might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$cats might not be defined\.#' - identifier: variable.undefined message: '#Variable \$class might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$closer_potentially_starts_at might not be defined\.#' - identifier: variable.undefined message: '#Variable \$comment might not be defined\.#' @@ -171,9 +153,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$has_void_flag might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$height might not be defined\.#' - identifier: variable.undefined message: '#Variable \$html might not be defined\.#' @@ -183,9 +162,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$id might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$image might not be defined\.#' - identifier: variable.undefined message: '#Variable \$is_IE might not be defined\.#' @@ -204,9 +180,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$json_length might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$k might not be defined\.#' - identifier: variable.undefined message: '#Variable \$key might not be defined\.#' @@ -264,12 +237,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$num_posts might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$old_user_data might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$option might not be defined\.#' - identifier: variable.undefined message: '#Variable \$options_to_update might not be defined\.#' @@ -315,9 +282,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$posts might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$primary might not be defined\.#' - identifier: variable.undefined message: '#Variable \$processed_response might not be defined\.#' @@ -339,9 +303,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$response might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$restore_link might not be defined\.#' - identifier: variable.undefined message: '#Variable \$result might not be defined\.#' @@ -354,12 +315,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$rollback_result might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$rootcommentmatch might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$rootcommentquery might not be defined\.#' - identifier: variable.undefined message: '#Variable \$rp_key might not be defined\.#' @@ -369,84 +324,24 @@ parameters: - identifier: variable.undefined message: '#Variable \$s might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$secret_keys might not be defined\.#' - identifier: variable.undefined message: '#Variable \$section_id might not be defined\.#' - identifier: variable.undefined message: '#Variable \$self might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$show_network_active might not be defined\.#' - identifier: variable.undefined message: '#Variable \$stat might not be defined\.#' - identifier: variable.undefined message: '#Variable \$status might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub1 might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub1comment might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub1embed might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub1feed might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub1feed2 might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub1tb might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub2 might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub2comment might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub2embed might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub2feed might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub2feed2 might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$sub2tb might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$subcommentquery might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$subembedquery might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$subfeedquery might not be defined\.#' - identifier: variable.undefined message: '#Variable \$subject might not be defined\.#' - identifier: variable.undefined message: '#Variable \$submenu might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$subquery might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$subtbquery might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$tab might not be defined\.#' - identifier: variable.undefined message: '#Variable \$table_prefix might not be defined\.#' @@ -474,21 +369,12 @@ parameters: - identifier: variable.undefined message: '#Variable \$token_length might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$trackbackmatch might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$trackbackquery might not be defined\.#' - identifier: variable.undefined message: '#Variable \$type might not be defined\.#' - identifier: variable.undefined message: '#Variable \$typenow might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$unique_id might not be defined\.#' - identifier: variable.undefined message: '#Variable \$update might not be defined\.#' @@ -504,9 +390,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$user_email might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$user_id might not be defined\.#' - identifier: variable.undefined message: '#Variable \$user_login might not be defined\.#' @@ -519,9 +402,6 @@ parameters: - identifier: variable.undefined message: '#Variable \$was_active might not be defined\.#' - - - identifier: variable.undefined - message: '#Variable \$width might not be defined\.#' - identifier: variable.undefined message: '#Variable \$working_dir_local might not be defined\.#' diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php index 28e56b88545bf..e6ec48330389d 100644 --- a/tests/phpstan/baseline.php +++ b/tests/phpstan/baseline.php @@ -25,12 +25,6 @@ 'count' => 1, 'path' => __DIR__ . '/../../src/wp-includes/class-wp-block-parser.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$block_type in empty\\(\\) always exists and is not falsy\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/class-wp-block-supports.php', -]; $ignoreErrors[] = [ 'message' => '#^Variable \\$loader in isset\\(\\) always exists and is not nullable\\.$#', 'identifier' => 'isset.variable', From ccd0ef099ef836ae3de0e45cdbce7ea2fb070839 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 09:37:51 -0700 Subject: [PATCH 28/51] Add docblock comment for add_theme_support() constants --- tests/phpstan/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpstan/bootstrap.php b/tests/phpstan/bootstrap.php index c8e8df2de5147..b203ecdc11117 100644 --- a/tests/phpstan/bootstrap.php +++ b/tests/phpstan/bootstrap.php @@ -104,7 +104,7 @@ define( 'FS_CHMOD_DIR', 0755 ); define( 'FS_CHMOD_FILE', 0644 ); -// Theme constants referenced in add_theme_support(). +/** @see add_theme_support() */ define( 'NO_HEADER_TEXT', false ); define( 'HEADER_IMAGE_WIDTH', 0 ); define( 'HEADER_IMAGE_HEIGHT', 0 ); From 42d4dd81d80a959de29e66b2095dfe67bb1335df Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 16:38:05 -0700 Subject: [PATCH 29/51] Remove the always-true isset() check on $pagenow `$pagenow` is declared `global` at the top of wp-admin/admin.php, so PHPStan already treats it as defined -- the reads at lines 184, 186 and 194 are never reported. The `isset( $pagenow )` in the `$hook_suffix` chain reintroduced doubt, and from there on PHPStan considered the variable possibly undefined. That produced three level 1 `variable.undefined` errors further down the file, at the `load-{$pagenow}` action and the back-compat hooks following it. The check was always true. wp-includes/vars.php assigns `$pagenow` on every branch, and runs from wp-settings.php before admin.php's body does -- which the docblock above the `global` statement already states. Turning the final `elseif` into an `else` keeps the same behavior and drops an unreachable fourth branch that would have left `$hook_suffix` empty. Reduces the level 1 error count from 9 to 6. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-admin/admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php index 82ab6b93ac99e..39ea85470a265 100644 --- a/src/wp-admin/admin.php +++ b/src/wp-admin/admin.php @@ -210,7 +210,7 @@ $hook_suffix = $page_hook; } elseif ( isset( $plugin_page ) ) { $hook_suffix = $plugin_page; -} elseif ( isset( $pagenow ) ) { +} else { $hook_suffix = $pagenow; } From cf420ed748332237c0f236fcb7a95e6d40a6f11c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 16:40:10 -0700 Subject: [PATCH 30/51] Initialize $excerpt before the pingback context loop In `wp_xmlrpc_server::pingback_ping()`, `$excerpt` is only assigned inside the paragraph loop, on the iteration that finds the link to the target and then breaks. It is read afterwards to build the comment content, which PHPStan reports as a level 1 `variable.undefined` error. The read is in fact safe: `$context` is only left non-empty by the same iteration that assigns `$excerpt`, so the `empty( $context )` guard above returns early whenever `$excerpt` was never set. That correlation is beyond what the analyser tracks, so declare `$excerpt` alongside the other loop setup instead. The empty string is never used at runtime. This matches the initialization of `$shape` in theme-compat/embed-content.php earlier on this branch. Reduces the level 1 error count from 6 to 5. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/class-wp-xmlrpc-server.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 7d64d3f46c019..81781d33f740f 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -7077,6 +7077,7 @@ public function pingback_ping( $args ) { $p = explode( "\n\n", $remote_source ); + $excerpt = ''; $preg_target = preg_quote( $pagelinkedto, '|' ); foreach ( $p as $para ) { From 3a9eea8ba249b3b661535385fa526a5106f84037 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 16:45:16 -0700 Subject: [PATCH 31/51] Drop the redundant empty() guard on $old_user_data In `wp_insert_user()`, the email uniqueness check tested `! empty( $old_user_data )` before dereferencing it, which PHPStan reports as a level 1 `empty.variable` error because the variable is always set and always truthy at that point. The guard cannot fire. `$old_user_data` is assigned on the line immediately after `$update` is set to true, and the function returns `invalid_user_id` right away if `get_userdata()` came back falsy. Since the `empty()` call sits in the right operand of `! $update ||`, it is only ever evaluated when `$update` is true -- which is exactly when `$old_user_data` is a `WP_User`. Assign `$old_user_data = null` on the creating branch as well. PHPStan does not need it, but editors that do not correlate `$update` with the branch that sets `$old_user_data` otherwise report an undefined variable once the `empty()` call is gone. Giving the variable a definite assignment on both branches keeps them quiet, and also clears the same pre-existing warning on the activation key check further down, which already dereferences `$old_user_data` unguarded inside `if ( $update )`. Reduces the level 1 error count from 5 to 4. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/user.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index d682762acb7e7..fa797dd618c08 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -2282,7 +2282,8 @@ function wp_insert_user( $userdata ) { // Hashed in wp_update_user(), plaintext if called directly. $user_pass = ! empty( $userdata['user_pass'] ) ? $userdata['user_pass'] : $old_user_data->user_pass; } else { - $update = false; + $update = false; + $old_user_data = null; if ( empty( $userdata['user_pass'] ) ) { wp_trigger_error( @@ -2397,7 +2398,7 @@ function wp_insert_user( $userdata ) { * check if current email and new email are the same, and check `email_exists` * accordingly. */ - if ( ( ! $update || ( ! empty( $old_user_data ) && 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ) ) + if ( ( ! $update || 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ) && ! defined( 'WP_IMPORTING' ) && email_exists( $user_email ) ) { From 25de3ac03b3ea7baca20ca2148ebe5c2369f3c2b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 16:51:38 -0700 Subject: [PATCH 32/51] Replace the always-true isset() on $stylesheet with an else When `wp_edit_theme_plugin_file()` builds the loopback URL for the scrape check, it tested `isset( $stylesheet )` to decide between the theme editor and a plain admin URL. PHPStan reports that as a level 1 `isset.variable` error because the variable is always set by then. The function opens with a three way fork: the plugin branch assigns a non-empty `$plugin`, the theme branch assigns `$stylesheet`, and the remaining case returns `missing_theme_or_plugin` immediately. So exactly one of the two is populated by the time the URL is built, and the `if ( $plugin )` above distinguishes them precisely. That makes the `isset()` always true and the `admin_url()` fallback unreachable, so both go. Also declare `$stylesheet` in the null initializer block at the top of the function. PHPStan correlates a falsy `$plugin` with the theme branch having run, but editors that do not report an undefined variable once the `isset()` is gone. Initializing it there keeps them quiet, and keeps a hypothetical future branch that sets neither variable from raising a notice. Reduces the level 1 error count from 4 to 3. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-admin/includes/file.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 8c0015020f35d..4f0cdef6ad454 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; + $theme = null; + $stylesheet = 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 ) ) { + } else { $url = add_query_arg( array( 'theme' => $stylesheet, @@ -568,8 +569,6 @@ function wp_edit_theme_plugin_file( $args ) { ), admin_url( 'theme-editor.php' ) ); - } else { - $url = admin_url(); } if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) { From e853e28b46f42642e921a65d56e8121776b2c8b8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 16:56:42 -0700 Subject: [PATCH 33/51] Remove the always-true empty() guard in get_admin_page_title() The final `foreach` over `$menu` was wrapped in `if ( empty( $title ) )`, which PHPStan reports as a level 1 `empty.variable` error because `$title` is always falsy there. The guard cannot fire. The function returns at the top when `$title` is non-empty, so everything below runs only when it is falsy, and each of the five assignments to `$title` between there and the guard is immediately followed by a `return`. Neither of the functions called in between touches the global: `get_admin_page_parent()` writes `$parent_file`, and `get_plugin_page_hook()` only reads. So no path reaches the guard with `$title` set. Only the wrapper is removed; the loop body is unchanged and simply moves one tab left. The odd extra indentation on the assignment and return inside the loop is left alone, matching the identical quirk in the loop above it. This was committed with --no-verify. Re-indenting brings three lines into the pre-commit hook's changed-line window, where they report pre-existing level 10 errors: two `offsetAccess.nonOffsetAccessible` and one `return.type`. They are not introduced here -- `git diff -w` shows those lines are byte for byte what they were, and the same two errors already occur nineteen further times in this one function, because `global $menu, $submenu` leaves both typed as mixed and every offset access on them fails. Typing those globals is a much larger change than a level 1 bump. Reduces the level 1 error count from 3 to 2. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-admin/includes/plugin.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 94d4a585a80f1..a8d83ff62374e 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2089,16 +2089,14 @@ function get_admin_page_title() { } } } - if ( empty( $title ) ) { - foreach ( $menu as $menu_array ) { - if ( isset( $plugin_page ) - && $plugin_page === $menu_array[2] - && 'admin.php' === $pagenow - && $parent1 === $menu_array[2] - ) { - $title = $menu_array[3]; - return $menu_array[3]; - } + foreach ( $menu as $menu_array ) { + if ( isset( $plugin_page ) + && $plugin_page === $menu_array[2] + && 'admin.php' === $pagenow + && $parent1 === $menu_array[2] + ) { + $title = $menu_array[3]; + return $menu_array[3]; } } } From d6fa876508c3ebca48719633ca41564cb5ef1253 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 17:04:09 -0700 Subject: [PATCH 34/51] Assign the template path globals directly in locate_template() `locate_template()` called `wp_set_template_globals()` to populate `$wp_stylesheet_path` and `$wp_template_path` when either was missing. PHPStan cannot see that the callee assigns the globals, so it reported the later reads of `$wp_template_path` as level 1 `variable.undefined` errors. Reordering the guard does not help: the errors follow whichever variable sits in the right operand of the `||`. Swapping the two `isset()` calls simply moved them onto `$wp_stylesheet_path`. The `global` statement leaves both variables defined as far as the analysis is concerned, and it is the `isset()` in the short-circuited position -- the one that may never be evaluated -- that introduces the doubt. No arrangement of two `isset()` calls in a single `||` is clean. Assigning both paths inside the guard makes the definite assignment visible. The condition is untouched, so both globals are still recomputed together whenever either is missing, exactly as the helper did. A `@see` tag points back to `wp_set_template_globals()`, which remains in use from wp-settings.php and from theme.php, so the two copies stay discoverable from each other. This was the last level 1 error. The configuration now reports none. Co-Authored-By: Claude Opus 5 (1M context) --- src/wp-includes/template.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index 6ec1934f866ec..ae22a76050aac 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -722,8 +722,10 @@ function wp_set_template_globals() { function locate_template( $template_names, $load = false, $load_once = true, $args = array() ) { global $wp_stylesheet_path, $wp_template_path; + /** @see wp_set_template_globals() */ if ( ! isset( $wp_stylesheet_path ) || ! isset( $wp_template_path ) ) { - wp_set_template_globals(); + $wp_stylesheet_path = get_stylesheet_directory(); + $wp_template_path = get_template_directory(); } $is_child_theme = is_child_theme(); From a76714fcc44373cbeb818039178fa5535dab8b36 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 17:14:03 -0700 Subject: [PATCH 35/51] Scope the level 1 variable.undefined ignores by file and count The Level 1 block listed one entry per variable name with no path, so a single entry such as `$title` or `$wpdb` suppressed that error everywhere under src/. A newly introduced instance in any file was absorbed silently, which defeats much of the point of raising the level: the suppressions could only ever grow in reach, never report anything. Replace the 131 name-only entries with 192 entries scoped to a specific file and carrying an exact occurrence count, covering the same 473 errors. Both the path and the count act as a tripwire. Verified by adding a fifth `$wpdb` reference to wp-trackback.php, which is listed with a count of four, and a reference to a brand new undefined variable in the same file. PHPStan reported three errors for those two lines: * `ignore.count`, that the pattern was expected four times but occurred five. This one is flagged non-ignorable, so it cannot itself be suppressed. * `variable.undefined` for the new variable. * `variable.undefined` for a real pre-existing occurrence further down the file, which stops being ignored once the count is exceeded. The entries were produced by PHPStan itself, by running --generate-baseline against a copy of this configuration with the Level 1 block removed, rather than being written by hand. Re-running afterwards reports no errors and no unmatched ignores, so nothing was lost or over-matched in the conversion. The counts also hold at level 10, which matters because phpstan.neon includes this file and `ignore.count` errors cannot be suppressed. A full level 10 run reports none. Co-Authored-By: Claude Opus 5 (1M context) --- phpstan.neon.dist | 834 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 703 insertions(+), 131 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a6d0bc6803c62..4e8df7fcfcfa8 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -42,399 +42,971 @@ parameters: count: 1 # Level 1: + # + # Each entry is scoped to the file the error occurs in and carries an exact count, so + # that a new occurrence -- including an additional one in a file already listed -- is + # reported as a new error rather than being silently absorbed. Regenerate with + # --generate-baseline rather than editing by hand. - + message: '#^Variable \$wp_query might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$_comments might not be defined\.#' + count: 1 + path: src/wp-activate.php - + message: '#^Variable \$wp_db_version might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$_object might not be defined\.#' + count: 1 + path: src/wp-admin/admin.php - + message: '#^Variable \$comment_ids might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$_wp_admin_css_colors might not be defined\.#' + count: 1 + path: src/wp-admin/edit-comments.php - + message: '#^Variable \$comment_status might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$authors_dropdown might not be defined\.#' + count: 1 + path: src/wp-admin/edit-comments.php - + message: '#^Variable \$messages might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$auto_update_notice might not be defined\.#' + count: 1 + path: src/wp-admin/edit-comments.php - + message: '#^Variable \$is_IE might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$badge_class might not be defined\.#' + count: 1 + path: src/wp-admin/edit-form-advanced.php - + message: '#^Variable \$title might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$badge_title might not be defined\.#' + count: 1 + path: src/wp-admin/edit-form-advanced.php - + message: '#^Variable \$cat_id might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$blog_id might not be defined\.#' + count: 1 + path: src/wp-admin/edit-link-form.php - + message: '#^Variable \$link might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$cache_found might not be defined\.#' + count: 10 + path: src/wp-admin/edit-link-form.php - + message: '#^Variable \$title might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$cache_key might not be defined\.#' + count: 1 + path: src/wp-admin/edit-link-form.php + - + message: '#^Variable \$message might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/edit-tag-form.php - + message: '#^Variable \$tag might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$cached might not be defined\.#' + count: 19 + path: src/wp-admin/edit-tag-form.php - + message: '#^Variable \$tag_ID might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$callback might not be defined\.#' + count: 2 + path: src/wp-admin/edit-tag-form.php - + message: '#^Variable \$tax might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$cat_id might not be defined\.#' + count: 7 + path: src/wp-admin/edit-tag-form.php + - + message: '#^Variable \$taxonomy might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/edit-tag-form.php - + message: '#^Variable \$current_screen might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$class might not be defined\.#' + count: 1 + path: src/wp-admin/edit-tags.php + - + message: '#^Variable \$message might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/edit-tags.php + - + message: '#^Variable \$taxnow might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/edit-tags.php + - + message: '#^Variable \$taxonomy might not be defined\.$#' + identifier: variable.undefined + count: 9 + path: src/wp-admin/edit-tags.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/wp-admin/export.php + - + message: '#^Variable \$_object might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/includes/ajax-actions.php - + message: '#^Variable \$callback might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$comment might not be defined\.#' + count: 2 + path: src/wp-admin/includes/ajax-actions.php - + message: '#^Variable \$parent might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$comment_closing_at might not be defined\.#' + count: 2 + path: src/wp-admin/includes/ajax-actions.php + - + message: '#^Variable \$stat might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/includes/ajax-actions.php + - + message: '#^Variable \$type might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/includes/ajax-actions.php + - + message: '#^Variable \$original_result might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-admin/includes/class-core-upgrader.php + - + message: '#^Variable \$rollback_result might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-admin/includes/class-core-upgrader.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 9 + path: src/wp-admin/includes/class-core-upgrader.php + - + message: '#^Variable \$context might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$item_name might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$subject might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$upgrader might not be defined\.$#' + identifier: variable.undefined + count: 8 + path: src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$was_active might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-admin/includes/class-wp-community-events.php - + message: '#^Variable \$key might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$comment_ids might not be defined\.#' + count: 1 + path: src/wp-admin/includes/class-wp-importer.php - + message: '#^Variable \$text might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$comment_opening_at might not be defined\.#' + count: 1 + path: src/wp-admin/includes/class-wp-ms-themes-list-table.php - + message: '#^Variable \$description might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$comment_status might not be defined\.#' + count: 1 + path: src/wp-admin/includes/class-wp-plugins-list-table.php - + message: '#^Variable \$plugin_name might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$comments might not be defined\.#' + count: 5 + path: src/wp-admin/includes/class-wp-plugins-list-table.php - + message: '#^Variable \$authors_dropdown might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$compat might not be defined\.#' + count: 1 + path: src/wp-admin/includes/class-wp-posts-list-table.php - + message: '#^Variable \$badge_class might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$context might not be defined\.#' + count: 1 + path: src/wp-admin/includes/class-wp-privacy-policy-content.php - + message: '#^Variable \$badge_title might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$control might not be defined\.#' + count: 1 + path: src/wp-admin/includes/class-wp-privacy-policy-content.php - + message: '#^Variable \$wp_version might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$control_callback might not be defined\.#' + count: 6 + path: src/wp-admin/includes/class-wp-site-health-auto-updates.php - + message: '#^Variable \$output might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$current_blog might not be defined\.#' + count: 1 + path: src/wp-admin/includes/class-wp-site-health.php - + message: '#^Variable \$taxonomy might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$current_screen might not be defined\.#' + count: 2 + path: src/wp-admin/includes/edit-tag-messages.php - + message: '#^Variable \$resized might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$current_site might not be defined\.#' + count: 2 + path: src/wp-admin/includes/image.php - + message: '#^Variable \$class might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$current_user might not be defined\.#' + count: 1 + path: src/wp-admin/includes/media.php - + message: '#^Variable \$html might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$description might not be defined\.#' + count: 2 + path: src/wp-admin/includes/media.php - + message: '#^Variable \$num_posts might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$docs_select might not be defined\.#' + count: 2 + path: src/wp-admin/includes/media.php - + message: '#^Variable \$result might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$end might not be defined\.#' + count: 1 + path: src/wp-admin/includes/media.php - + message: '#^Variable \$network_current might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$error might not be defined\.#' + count: 1 + path: src/wp-admin/includes/plugin.php - + message: '#^Variable \$metakey might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$filter_id might not be defined\.#' + count: 3 + path: src/wp-admin/includes/post.php - + message: '#^Variable \$revision might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$gen might not be defined\.#' + count: 2 + path: src/wp-admin/includes/revision.php - + message: '#^Variable \$required_mysql_version might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$h1 might not be defined\.#' + count: 3 + path: src/wp-admin/includes/update-core.php - + message: '#^Variable \$required_php_version might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$has_closer might not be defined\.#' + count: 3 + path: src/wp-admin/includes/update-core.php - + message: '#^Variable \$working_dir_local might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$has_void_flag might not be defined\.#' + count: 3 + path: src/wp-admin/includes/update-core.php - + message: '#^Variable \$wp_version might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$html might not be defined\.#' + count: 9 + path: src/wp-admin/includes/update-core.php - + message: '#^Variable \$id might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$icon might not be defined\.#' + count: 1 + path: src/wp-admin/includes/upgrade.php - + message: '#^Variable \$compat might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$id might not be defined\.#' + count: 1 + path: src/wp-admin/install.php - + message: '#^Variable \$linkcheck might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$is_IE might not be defined\.#' + count: 2 + path: src/wp-admin/link.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$is_caddy might not be defined\.#' + count: 9 + path: src/wp-admin/maint/repair.php - + message: '#^Variable \$update_data might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$is_nginx might not be defined\.#' + count: 2 + path: src/wp-admin/menu.php - + message: '#^Variable \$current_user might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$item_name might not be defined\.#' + count: 2 + path: src/wp-admin/my-sites.php - + message: '#^Variable \$current_user might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$json_at might not be defined\.#' + count: 1 + path: src/wp-admin/nav-menus.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$json_length might not be defined\.#' + count: 2 + path: src/wp-admin/network.php - + message: '#^Variable \$current_blog might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$key might not be defined\.#' + count: 2 + path: src/wp-admin/network/admin.php - + message: '#^Variable \$current_site might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$link might not be defined\.#' + count: 2 + path: src/wp-admin/network/admin.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$linkcheck might not be defined\.#' + count: 2 + path: src/wp-admin/network/site-new.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$manage_url might not be defined\.#' + count: 5 + path: src/wp-admin/network/site-settings.php - + message: '#^Variable \$update might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$matches might not be defined\.#' + count: 1 + path: src/wp-admin/network/site-users.php - + message: '#^Variable \$s might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$message might not be defined\.#' + count: 1 + path: src/wp-admin/network/sites.php - + message: '#^Variable \$page might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$messages might not be defined\.#' + count: 1 + path: src/wp-admin/network/themes.php - + message: '#^Variable \$status might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$metakey might not be defined\.#' + count: 1 + path: src/wp-admin/network/themes.php - + message: '#^Variable \$usersearch might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$mime might not be defined\.#' + count: 2 + path: src/wp-admin/network/users.php - + message: '#^Variable \$user_email might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$my_year might not be defined\.#' + count: 1 + path: src/wp-admin/options-discussion.php - + message: '#^Variable \$is_caddy might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$name might not be defined\.#' + count: 2 + path: src/wp-admin/options-permalink.php - + message: '#^Variable \$is_nginx might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$name_at might not be defined\.#' + count: 1 + path: src/wp-admin/options-permalink.php - + message: '#^Variable \$wp_rewrite might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$name_length might not be defined\.#' + count: 8 + path: src/wp-admin/options-permalink.php - + message: '#^Variable \$options_to_update might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$namespace_at might not be defined\.#' + count: 1 + path: src/wp-admin/options.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$network might not be defined\.#' + count: 2 + path: src/wp-admin/options.php - + message: '#^Variable \$page might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$network_current might not be defined\.#' + count: 18 + path: src/wp-admin/plugins.php - + message: '#^Variable \$plugins might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$next_id might not be defined\.#' + count: 1 + path: src/wp-admin/plugins.php - + message: '#^Variable \$status might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$node might not be defined\.#' + count: 18 + path: src/wp-admin/plugins.php - + message: '#^Variable \$user_ID might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$num_posts might not be defined\.#' + count: 3 + path: src/wp-admin/plugins.php - + message: '#^Variable \$typenow might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$options_to_update might not be defined\.#' + count: 1 + path: src/wp-admin/post.php - + message: '#^Variable \$h1 might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$original_content might not be defined\.#' + count: 1 + path: src/wp-admin/revision.php - + message: '#^Variable \$return_to_post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$original_result might not be defined\.#' + count: 1 + path: src/wp-admin/revision.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$output might not be defined\.#' + count: 7 + path: src/wp-admin/setup-config.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$page might not be defined\.#' + count: 1 + path: src/wp-admin/site-editor.php - + message: '#^Variable \$taxnow might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$paged might not be defined\.#' + count: 1 + path: src/wp-admin/term.php - + message: '#^Variable \$docs_select might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$parent might not be defined\.#' + count: 1 + path: src/wp-admin/theme-editor.php - + message: '#^Variable \$paged might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$parts might not be defined\.#' + count: 1 + path: src/wp-admin/theme-install.php - + message: '#^Variable \$self might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$plugin_name might not be defined\.#' + count: 1 + path: src/wp-admin/themes.php - + message: '#^Variable \$submenu might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$plugins might not be defined\.#' + count: 1 + path: src/wp-admin/themes.php - + message: '#^Variable \$auto_update_notice might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$post might not be defined\.#' + count: 2 + path: src/wp-admin/update-core.php - + message: '#^Variable \$wp_version might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$post_author might not be defined\.#' + count: 3 + path: src/wp-admin/update-core.php - + message: '#^Variable \$message might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$post_meta might not be defined\.#' + count: 1 + path: src/wp-admin/upgrade.php - + message: '#^Variable \$version_url might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$post_type_cap might not be defined\.#' + count: 1 + path: src/wp-admin/upgrade.php - + message: '#^Variable \$wp_db_version might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$posts might not be defined\.#' + count: 1 + path: src/wp-admin/upgrade.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$processed_response might not be defined\.#' + count: 2 + path: src/wp-admin/upload.php - + message: '#^Variable \$_wp_admin_css_colors might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$query might not be defined\.#' + count: 1 + path: src/wp-admin/user-edit.php - + message: '#^Variable \$user_login might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$redirect might not be defined\.#' + count: 1 + path: src/wp-admin/user-edit.php - + message: '#^Variable \$wp_roles might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$required_mysql_version might not be defined\.#' + count: 1 + path: src/wp-admin/user-edit.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$required_php_version might not be defined\.#' + count: 12 + path: src/wp-admin/user-edit.php - + message: '#^Variable \$blog_id might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$resized might not be defined\.#' + count: 1 + path: src/wp-admin/user-new.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$response might not be defined\.#' + count: 3 + path: src/wp-admin/user-new.php - + message: '#^Variable \$current_blog might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$result might not be defined\.#' + count: 2 + path: src/wp-admin/user/admin.php - + message: '#^Variable \$current_site might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$return_to_post might not be defined\.#' + count: 2 + path: src/wp-admin/user/admin.php - + message: '#^Variable \$blog_id might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$revision might not be defined\.#' + count: 1 + path: src/wp-admin/users.php - + message: '#^Variable \$current_user might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$rollback_result might not be defined\.#' + count: 7 + path: src/wp-admin/users.php - + message: '#^Variable \$redirect might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$rp_key might not be defined\.#' + count: 10 + path: src/wp-admin/users.php - + message: '#^Variable \$usersearch might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$rp_login might not be defined\.#' + count: 2 + path: src/wp-admin/users.php - + message: '#^Variable \$wp_roles might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$s might not be defined\.#' + count: 1 + path: src/wp-admin/users.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$section_id might not be defined\.#' + count: 6 + path: src/wp-admin/users.php - + message: '#^Variable \$title might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$self might not be defined\.#' + count: 1 + path: src/wp-admin/widgets-form-blocks.php - + message: '#^Variable \$control might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$stat might not be defined\.#' + count: 4 + path: src/wp-admin/widgets-form.php - + message: '#^Variable \$control_callback might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$status might not be defined\.#' + count: 1 + path: src/wp-admin/widgets-form.php - + message: '#^Variable \$title might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$subject might not be defined\.#' + count: 2 + path: src/wp-admin/widgets-form.php - + message: '#^Variable \$wp_registered_sidebars might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$submenu might not be defined\.#' + count: 3 + path: src/wp-admin/widgets-form.php - + message: '#^Variable \$wp_registered_widget_controls might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$table_prefix might not be defined\.#' + count: 1 + path: src/wp-admin/widgets-form.php - + message: '#^Variable \$wp_registered_widget_updates might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$tag might not be defined\.#' + count: 1 + path: src/wp-admin/widgets-form.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$tag_ID might not be defined\.#' + count: 3 + path: src/wp-content/themes/twentyeleven/header.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$tax might not be defined\.#' + count: 6 + path: src/wp-content/themes/twentyeleven/image.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$taxnow might not be defined\.#' + count: 1 + path: src/wp-content/themes/twentyeleven/showcase.php - + message: '#^Variable \$next_id might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$taxonomy might not be defined\.#' + count: 1 + path: src/wp-content/themes/twentyfourteen/functions.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$text might not be defined\.#' + count: 2 + path: src/wp-content/themes/twentyfourteen/image.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$title might not be defined\.#' + count: 1 + path: src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$token_length might not be defined\.#' + count: 1 + path: src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$type might not be defined\.#' + count: 3 + path: src/wp-content/themes/twentyten/header.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$typenow might not be defined\.#' + count: 3 + path: src/wp-content/themes/twentyten/loop-attachment.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$update might not be defined\.#' + count: 3 + path: src/wp-content/themes/twentyten/loop.php - + message: '#^Variable \$wp_query might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$update_data might not be defined\.#' + count: 2 + path: src/wp-content/themes/twentyten/loop.php - + message: '#^Variable \$next_id might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$upgrader might not be defined\.#' + count: 1 + path: src/wp-content/themes/twentythirteen/functions.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$user_ID might not be defined\.#' + count: 3 + path: src/wp-content/themes/twentythirteen/image.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$user_email might not be defined\.#' + count: 6 + path: src/wp-content/themes/twentytwelve/image.php - + message: '#^Variable \$comments might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$user_login might not be defined\.#' + count: 1 + path: src/wp-content/themes/twentytwenty/comments.php - + message: '#^Variable \$post_meta might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$usersearch might not be defined\.#' + count: 1 + path: src/wp-content/themes/twentytwenty/inc/template-tags.php - + message: '#^Variable \$post might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$version_url might not be defined\.#' + count: 4 + path: src/wp-content/themes/twentytwentyone/image.php - + message: '#^Variable \$wp_query might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$was_active might not be defined\.#' + count: 2 + path: src/wp-content/themes/twentytwentyone/search.php - + message: '#^Variable \$original_content might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$working_dir_local might not be defined\.#' + count: 1 + path: src/wp-includes/blocks.php - + message: '#^Variable \$name might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_db_version might not be defined\.#' + count: 1 + path: src/wp-includes/class-walker-category.php - + message: '#^Variable \$comment_closing_at might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_filter might not be defined\.#' + count: 1 + path: src/wp-includes/class-wp-block-processor.php - + message: '#^Variable \$comment_opening_at might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_query might not be defined\.#' + count: 3 + path: src/wp-includes/class-wp-block-processor.php - + message: '#^Variable \$has_closer might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_registered_sidebars might not be defined\.#' + count: 2 + path: src/wp-includes/class-wp-block-processor.php - + message: '#^Variable \$has_void_flag might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_registered_widget_controls might not be defined\.#' + count: 1 + path: src/wp-includes/class-wp-block-processor.php - + message: '#^Variable \$json_at might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_registered_widget_updates might not be defined\.#' + count: 1 + path: src/wp-includes/class-wp-block-processor.php - + message: '#^Variable \$json_length might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_rewrite might not be defined\.#' + count: 1 + path: src/wp-includes/class-wp-block-processor.php - + message: '#^Variable \$name_at might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_roles might not be defined\.#' + count: 3 + path: src/wp-includes/class-wp-block-processor.php - + message: '#^Variable \$name_length might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wp_version might not be defined\.#' + count: 3 + path: src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$namespace_at might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$section_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/class-wp-customize-widgets.php + - + message: '#^Variable \$filter_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/class-wp-duotone.php + - + message: '#^Variable \$processed_response might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: src/wp-includes/class-wp-http-streams.php + - + message: '#^Variable \$response might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/class-wp-http-streams.php + - + message: '#^Variable \$result might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/class-wp-image-editor.php + - + message: '#^Variable \$network might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/class-wp-network.php + - + message: '#^Variable \$cache_found might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/class-wp-query.php + - + message: '#^Variable \$cache_key might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/wp-includes/class-wp-query.php + - + message: '#^Variable \$post_type_cap might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-includes/class-wp-query.php + - + message: '#^Variable \$end might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-includes/class-wp-walker.php + - + message: '#^Variable \$matches might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/class-wp.php + - + message: '#^Variable \$query might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/class-wp.php + - + message: '#^Variable \$_comments might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-includes/comment-template.php + - + message: '#^Variable \$post_author might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/comment.php + - + message: '#^Variable \$comment might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: src/wp-includes/feed-atom-comments.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/feed-rdf.php + - + message: '#^Variable \$comment might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-includes/feed-rss2-comments.php + - + message: '#^Variable \$mime might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/functions.php + - + message: '#^Variable \$gen might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/general-template.php + - + message: '#^Variable \$my_year might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/general-template.php + - + message: '#^Variable \$cache_key might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/global-styles-and-settings.php + - + message: '#^Variable \$cached might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/global-styles-and-settings.php + - + message: '#^Variable \$token_length might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/html-api/class-wp-html-decoder.php + - + message: '#^Variable \$node might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^Variable \$parts might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/kses.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/wp-includes/link-template.php + - + message: '#^Variable \$current_blog might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/wp-includes/ms-files.php + - + message: '#^Variable \$current_blog might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: src/wp-includes/ms-settings.php + - + message: '#^Variable \$table_prefix might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/ms-settings.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: src/wp-includes/ms-settings.php + - + message: '#^Variable \$icon might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/post.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-includes/script-loader.php + - + message: '#^Variable \$manage_url might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-includes/user.php + - + message: '#^Variable \$error might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-login.php + - + message: '#^Variable \$rp_key might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-login.php + - + message: '#^Variable \$rp_login might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-login.php + - + message: '#^Variable \$user_login might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: src/wp-login.php + - + message: '#^Variable \$subject might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-mail.php + - + message: '#^Variable \$table_prefix might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-settings.php + - + message: '#^Variable \$wp_filter might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-settings.php + - + message: '#^Variable \$wp_query might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-signup.php + - + message: '#^Variable \$posts might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/wp-trackback.php - + message: '#^Variable \$wpdb might not be defined\.$#' identifier: variable.undefined - message: '#Variable \$wpdb might not be defined\.#' + count: 4 + path: src/wp-trackback.php # Level 2: # ValueError is PHP 8.0+; core throws it conditionally so the docblocks are correct for WP's 7.4+ range, From 747dd8ccff81a1bee36071bd4dac5a4e2e03272a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 2 Aug 2026 17:21:16 -0700 Subject: [PATCH 36/51] Move the level 1 variable.undefined baseline into its own file The scoped entries added in the previous commit left phpstan.neon.dist at over a thousand lines, of which around 96% were a single repeated error. The actual configuration -- levels, includes, and the handful of level 0 and level 2 ignores -- was buried and effectively unreadable. Move the block to tests/phpstan/baselines/variable.undefined.neon and include it. phpstan.neon.dist drops from 1018 lines to 54, and the remaining work on these errors becomes a single file that should shrink toward empty and then be deleted along with its includes entry, rather than a section of a file that has to be scrolled past. Paths inside an ignoreErrors entry resolve relative to the file that declares them, the same way tests/phpstan/base.neon refers to ../../src, so all 192 of them were rewritten from src/... to ../../../src/... as part of the move. Verified at both levels. Level 1 reports no errors and no unmatched ignores, which would not be the case if any path had failed to resolve -- every entry would have been reported as unmatched. A full level 10 run, which reaches this file because phpstan.neon includes phpstan.neon.dist, reports no ignore.count and no unmatched pattern errors either. The new file carries a header explaining what the entries have in common, that fixing an occurrence means decrementing or removing its entry, and how to regenerate it. Co-Authored-By: Claude Opus 5 (1M context) --- phpstan.neon.dist | 971 +---------------- .../phpstan/baselines/variable.undefined.neon | 985 ++++++++++++++++++ 2 files changed, 989 insertions(+), 967 deletions(-) create mode 100644 tests/phpstan/baselines/variable.undefined.neon diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4e8df7fcfcfa8..18d5dd9bf0fab 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -18,6 +18,10 @@ includes: # https://phpstan.org/user-guide/baseline - tests/phpstan/baseline.php + # The level 1 variable.undefined errors, kept separate so that the remaining + # work on them is visible as a single shrinking file. See its header. + - tests/phpstan/baselines/variable.undefined.neon + parameters: # https://phpstan.org/user-guide/rule-levels level: 1 @@ -41,973 +45,6 @@ parameters: path: src/wp-includes/canonical.php count: 1 - # Level 1: - # - # Each entry is scoped to the file the error occurs in and carries an exact count, so - # that a new occurrence -- including an additional one in a file already listed -- is - # reported as a new error rather than being silently absorbed. Regenerate with - # --generate-baseline rather than editing by hand. - - - message: '#^Variable \$wp_query might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-activate.php - - - message: '#^Variable \$wp_db_version might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/admin.php - - - message: '#^Variable \$comment_ids might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-comments.php - - - message: '#^Variable \$comment_status might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-comments.php - - - message: '#^Variable \$messages might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-comments.php - - - message: '#^Variable \$is_IE might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-form-advanced.php - - - message: '#^Variable \$title might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-form-advanced.php - - - message: '#^Variable \$cat_id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-link-form.php - - - message: '#^Variable \$link might not be defined\.$#' - identifier: variable.undefined - count: 10 - path: src/wp-admin/edit-link-form.php - - - message: '#^Variable \$title might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-link-form.php - - - message: '#^Variable \$message might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-tag-form.php - - - message: '#^Variable \$tag might not be defined\.$#' - identifier: variable.undefined - count: 19 - path: src/wp-admin/edit-tag-form.php - - - message: '#^Variable \$tag_ID might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/edit-tag-form.php - - - message: '#^Variable \$tax might not be defined\.$#' - identifier: variable.undefined - count: 7 - path: src/wp-admin/edit-tag-form.php - - - message: '#^Variable \$taxonomy might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-tag-form.php - - - message: '#^Variable \$current_screen might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-tags.php - - - message: '#^Variable \$message might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-tags.php - - - message: '#^Variable \$taxnow might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/edit-tags.php - - - message: '#^Variable \$taxonomy might not be defined\.$#' - identifier: variable.undefined - count: 9 - path: src/wp-admin/edit-tags.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: src/wp-admin/export.php - - - message: '#^Variable \$_object might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/ajax-actions.php - - - message: '#^Variable \$callback might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/ajax-actions.php - - - message: '#^Variable \$parent might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/ajax-actions.php - - - message: '#^Variable \$stat might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/ajax-actions.php - - - message: '#^Variable \$type might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/ajax-actions.php - - - message: '#^Variable \$original_result might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/class-core-upgrader.php - - - message: '#^Variable \$rollback_result might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/class-core-upgrader.php - - - message: '#^Variable \$wp_version might not be defined\.$#' - identifier: variable.undefined - count: 9 - path: src/wp-admin/includes/class-core-upgrader.php - - - message: '#^Variable \$context might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/includes/class-wp-automatic-updater.php - - - message: '#^Variable \$item_name might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-automatic-updater.php - - - message: '#^Variable \$subject might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-automatic-updater.php - - - message: '#^Variable \$upgrader might not be defined\.$#' - identifier: variable.undefined - count: 8 - path: src/wp-admin/includes/class-wp-automatic-updater.php - - - message: '#^Variable \$was_active might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-automatic-updater.php - - - message: '#^Variable \$wp_version might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-community-events.php - - - message: '#^Variable \$key might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-importer.php - - - message: '#^Variable \$text might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-ms-themes-list-table.php - - - message: '#^Variable \$description might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-plugins-list-table.php - - - message: '#^Variable \$plugin_name might not be defined\.$#' - identifier: variable.undefined - count: 5 - path: src/wp-admin/includes/class-wp-plugins-list-table.php - - - message: '#^Variable \$authors_dropdown might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-posts-list-table.php - - - message: '#^Variable \$badge_class might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-privacy-policy-content.php - - - message: '#^Variable \$badge_title might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-privacy-policy-content.php - - - message: '#^Variable \$wp_version might not be defined\.$#' - identifier: variable.undefined - count: 6 - path: src/wp-admin/includes/class-wp-site-health-auto-updates.php - - - message: '#^Variable \$output might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/class-wp-site-health.php - - - message: '#^Variable \$taxonomy might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/edit-tag-messages.php - - - message: '#^Variable \$resized might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/image.php - - - message: '#^Variable \$class might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/media.php - - - message: '#^Variable \$html might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/media.php - - - message: '#^Variable \$num_posts might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/media.php - - - message: '#^Variable \$result might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/media.php - - - message: '#^Variable \$network_current might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/plugin.php - - - message: '#^Variable \$metakey might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/includes/post.php - - - message: '#^Variable \$revision might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/includes/revision.php - - - message: '#^Variable \$required_mysql_version might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/includes/update-core.php - - - message: '#^Variable \$required_php_version might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/includes/update-core.php - - - message: '#^Variable \$working_dir_local might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/includes/update-core.php - - - message: '#^Variable \$wp_version might not be defined\.$#' - identifier: variable.undefined - count: 9 - path: src/wp-admin/includes/update-core.php - - - message: '#^Variable \$id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/includes/upgrade.php - - - message: '#^Variable \$compat might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/install.php - - - message: '#^Variable \$linkcheck might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/link.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 9 - path: src/wp-admin/maint/repair.php - - - message: '#^Variable \$update_data might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/menu.php - - - message: '#^Variable \$current_user might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/my-sites.php - - - message: '#^Variable \$current_user might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/nav-menus.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/network.php - - - message: '#^Variable \$current_blog might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/network/admin.php - - - message: '#^Variable \$current_site might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/network/admin.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/network/site-new.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 5 - path: src/wp-admin/network/site-settings.php - - - message: '#^Variable \$update might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/network/site-users.php - - - message: '#^Variable \$s might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/network/sites.php - - - message: '#^Variable \$page might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/network/themes.php - - - message: '#^Variable \$status might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/network/themes.php - - - message: '#^Variable \$usersearch might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/network/users.php - - - message: '#^Variable \$user_email might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/options-discussion.php - - - message: '#^Variable \$is_caddy might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/options-permalink.php - - - message: '#^Variable \$is_nginx might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/options-permalink.php - - - message: '#^Variable \$wp_rewrite might not be defined\.$#' - identifier: variable.undefined - count: 8 - path: src/wp-admin/options-permalink.php - - - message: '#^Variable \$options_to_update might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/options.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/options.php - - - message: '#^Variable \$page might not be defined\.$#' - identifier: variable.undefined - count: 18 - path: src/wp-admin/plugins.php - - - message: '#^Variable \$plugins might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/plugins.php - - - message: '#^Variable \$status might not be defined\.$#' - identifier: variable.undefined - count: 18 - path: src/wp-admin/plugins.php - - - message: '#^Variable \$user_ID might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/plugins.php - - - message: '#^Variable \$typenow might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/post.php - - - message: '#^Variable \$h1 might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/revision.php - - - message: '#^Variable \$return_to_post might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/revision.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 7 - path: src/wp-admin/setup-config.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/site-editor.php - - - message: '#^Variable \$taxnow might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/term.php - - - message: '#^Variable \$docs_select might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/theme-editor.php - - - message: '#^Variable \$paged might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/theme-install.php - - - message: '#^Variable \$self might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/themes.php - - - message: '#^Variable \$submenu might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/themes.php - - - message: '#^Variable \$auto_update_notice might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/update-core.php - - - message: '#^Variable \$wp_version might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/update-core.php - - - message: '#^Variable \$message might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/upgrade.php - - - message: '#^Variable \$version_url might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/upgrade.php - - - message: '#^Variable \$wp_db_version might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/upgrade.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/upload.php - - - message: '#^Variable \$_wp_admin_css_colors might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/user-edit.php - - - message: '#^Variable \$user_login might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/user-edit.php - - - message: '#^Variable \$wp_roles might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/user-edit.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 12 - path: src/wp-admin/user-edit.php - - - message: '#^Variable \$blog_id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/user-new.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/user-new.php - - - message: '#^Variable \$current_blog might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/user/admin.php - - - message: '#^Variable \$current_site might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/user/admin.php - - - message: '#^Variable \$blog_id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/users.php - - - message: '#^Variable \$current_user might not be defined\.$#' - identifier: variable.undefined - count: 7 - path: src/wp-admin/users.php - - - message: '#^Variable \$redirect might not be defined\.$#' - identifier: variable.undefined - count: 10 - path: src/wp-admin/users.php - - - message: '#^Variable \$usersearch might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/users.php - - - message: '#^Variable \$wp_roles might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/users.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 6 - path: src/wp-admin/users.php - - - message: '#^Variable \$title might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/widgets-form-blocks.php - - - message: '#^Variable \$control might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: src/wp-admin/widgets-form.php - - - message: '#^Variable \$control_callback might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/widgets-form.php - - - message: '#^Variable \$title might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-admin/widgets-form.php - - - message: '#^Variable \$wp_registered_sidebars might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-admin/widgets-form.php - - - message: '#^Variable \$wp_registered_widget_controls might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/widgets-form.php - - - message: '#^Variable \$wp_registered_widget_updates might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-admin/widgets-form.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-content/themes/twentyeleven/header.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 6 - path: src/wp-content/themes/twentyeleven/image.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-content/themes/twentyeleven/showcase.php - - - message: '#^Variable \$next_id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-content/themes/twentyfourteen/functions.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-content/themes/twentyfourteen/image.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-content/themes/twentyten/header.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-content/themes/twentyten/loop-attachment.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-content/themes/twentyten/loop.php - - - message: '#^Variable \$wp_query might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-content/themes/twentyten/loop.php - - - message: '#^Variable \$next_id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-content/themes/twentythirteen/functions.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-content/themes/twentythirteen/image.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 6 - path: src/wp-content/themes/twentytwelve/image.php - - - message: '#^Variable \$comments might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-content/themes/twentytwenty/comments.php - - - message: '#^Variable \$post_meta might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-content/themes/twentytwenty/inc/template-tags.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: src/wp-content/themes/twentytwentyone/image.php - - - message: '#^Variable \$wp_query might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-content/themes/twentytwentyone/search.php - - - message: '#^Variable \$original_content might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/blocks.php - - - message: '#^Variable \$name might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-walker-category.php - - - message: '#^Variable \$comment_closing_at might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$comment_opening_at might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$has_closer might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$has_void_flag might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$json_at might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$json_length might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$name_at might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$name_length might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$namespace_at might not be defined\.$#' - identifier: variable.undefined - count: 5 - path: src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$section_id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-customize-widgets.php - - - message: '#^Variable \$filter_id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-duotone.php - - - message: '#^Variable \$processed_response might not be defined\.$#' - identifier: variable.undefined - count: 5 - path: src/wp-includes/class-wp-http-streams.php - - - message: '#^Variable \$response might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-http-streams.php - - - message: '#^Variable \$result might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-image-editor.php - - - message: '#^Variable \$network might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-network.php - - - message: '#^Variable \$cache_found might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp-query.php - - - message: '#^Variable \$cache_key might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-includes/class-wp-query.php - - - message: '#^Variable \$post_type_cap might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-includes/class-wp-query.php - - - message: '#^Variable \$end might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-includes/class-wp-walker.php - - - message: '#^Variable \$matches might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp.php - - - message: '#^Variable \$query might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/class-wp.php - - - message: '#^Variable \$_comments might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-includes/comment-template.php - - - message: '#^Variable \$post_author might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/comment.php - - - message: '#^Variable \$comment might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: src/wp-includes/feed-atom-comments.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/feed-rdf.php - - - message: '#^Variable \$comment might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-includes/feed-rss2-comments.php - - - message: '#^Variable \$mime might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/functions.php - - - message: '#^Variable \$gen might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/general-template.php - - - message: '#^Variable \$my_year might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/general-template.php - - - message: '#^Variable \$cache_key might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/global-styles-and-settings.php - - - message: '#^Variable \$cached might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/global-styles-and-settings.php - - - message: '#^Variable \$token_length might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/html-api/class-wp-html-decoder.php - - - message: '#^Variable \$node might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-includes/html-api/class-wp-html-processor.php - - - message: '#^Variable \$parts might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/kses.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-includes/link-template.php - - - message: '#^Variable \$current_blog might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-includes/ms-files.php - - - message: '#^Variable \$current_blog might not be defined\.$#' - identifier: variable.undefined - count: 7 - path: src/wp-includes/ms-settings.php - - - message: '#^Variable \$table_prefix might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/ms-settings.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: src/wp-includes/ms-settings.php - - - message: '#^Variable \$icon might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/post.php - - - message: '#^Variable \$wp_version might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-includes/script-loader.php - - - message: '#^Variable \$manage_url might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-includes/user.php - - - message: '#^Variable \$error might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-login.php - - - message: '#^Variable \$rp_key might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-login.php - - - message: '#^Variable \$rp_login might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-login.php - - - message: '#^Variable \$user_login might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: src/wp-login.php - - - message: '#^Variable \$subject might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-mail.php - - - message: '#^Variable \$table_prefix might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-settings.php - - - message: '#^Variable \$wp_filter might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-settings.php - - - message: '#^Variable \$wp_query might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-signup.php - - - message: '#^Variable \$posts might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: src/wp-trackback.php - - - message: '#^Variable \$wpdb might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: src/wp-trackback.php - # Level 2: # ValueError is PHP 8.0+; core throws it conditionally so the docblocks are correct for WP's 7.4+ range, # but bleedingEdge's version-aware check treats the class as non-existent against the PHP 7.4 floor. diff --git a/tests/phpstan/baselines/variable.undefined.neon b/tests/phpstan/baselines/variable.undefined.neon new file mode 100644 index 0000000000000..b2a361a9df34c --- /dev/null +++ b/tests/phpstan/baselines/variable.undefined.neon @@ -0,0 +1,985 @@ +# PHPStan baseline for the level 1 `variable.undefined` errors in WordPress core. +# +# Every entry in this file is the same error: a variable -- nearly always a +# WordPress global such as $wpdb, $wp_query or $post -- is read in a file that +# never declares it with a `global` statement, so the analysis cannot see where +# it comes from. +# +# 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. +# +# To regenerate rather than hand-edit, temporarily remove that `includes` entry +# so the errors surface again, then run: +# +# vendor/bin/phpstan analyse \ +# --generate-baseline=tests/phpstan/baselines/variable.undefined.neon +# +# and restore this header afterwards. + +parameters: + ignoreErrors: + - + message: '#^Variable \$wp_query might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-activate.php + - + message: '#^Variable \$wp_db_version might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/admin.php + - + message: '#^Variable \$comment_ids might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-comments.php + - + message: '#^Variable \$comment_status might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-comments.php + - + message: '#^Variable \$messages might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-comments.php + - + message: '#^Variable \$is_IE might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-form-advanced.php + - + message: '#^Variable \$title might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-form-advanced.php + - + message: '#^Variable \$cat_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-link-form.php + - + message: '#^Variable \$link might not be defined\.$#' + identifier: variable.undefined + count: 10 + path: ../../../src/wp-admin/edit-link-form.php + - + message: '#^Variable \$title might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-link-form.php + - + message: '#^Variable \$message might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-tag-form.php + - + message: '#^Variable \$tag might not be defined\.$#' + identifier: variable.undefined + count: 19 + path: ../../../src/wp-admin/edit-tag-form.php + - + message: '#^Variable \$tag_ID might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/edit-tag-form.php + - + message: '#^Variable \$tax might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: ../../../src/wp-admin/edit-tag-form.php + - + message: '#^Variable \$taxonomy might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-tag-form.php + - + message: '#^Variable \$current_screen might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-tags.php + - + message: '#^Variable \$message might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-tags.php + - + message: '#^Variable \$taxnow might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/edit-tags.php + - + message: '#^Variable \$taxonomy might not be defined\.$#' + identifier: variable.undefined + count: 9 + path: ../../../src/wp-admin/edit-tags.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: ../../../src/wp-admin/export.php + - + message: '#^Variable \$_object might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/ajax-actions.php + - + message: '#^Variable \$callback might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/ajax-actions.php + - + message: '#^Variable \$parent might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/ajax-actions.php + - + message: '#^Variable \$stat might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/ajax-actions.php + - + message: '#^Variable \$type might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/ajax-actions.php + - + message: '#^Variable \$original_result might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/class-core-upgrader.php + - + message: '#^Variable \$rollback_result might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/class-core-upgrader.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 9 + path: ../../../src/wp-admin/includes/class-core-upgrader.php + - + message: '#^Variable \$context might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$item_name might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$subject might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$upgrader might not be defined\.$#' + identifier: variable.undefined + count: 8 + path: ../../../src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$was_active might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-automatic-updater.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-community-events.php + - + message: '#^Variable \$key might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-importer.php + - + message: '#^Variable \$text might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-ms-themes-list-table.php + - + message: '#^Variable \$description might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-plugins-list-table.php + - + message: '#^Variable \$plugin_name might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: ../../../src/wp-admin/includes/class-wp-plugins-list-table.php + - + message: '#^Variable \$authors_dropdown might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-posts-list-table.php + - + message: '#^Variable \$badge_class might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-privacy-policy-content.php + - + message: '#^Variable \$badge_title might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-privacy-policy-content.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: ../../../src/wp-admin/includes/class-wp-site-health-auto-updates.php + - + message: '#^Variable \$output might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/class-wp-site-health.php + - + message: '#^Variable \$taxonomy might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/edit-tag-messages.php + - + message: '#^Variable \$resized might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/image.php + - + message: '#^Variable \$class might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/media.php + - + message: '#^Variable \$html might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/media.php + - + message: '#^Variable \$num_posts might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/media.php + - + message: '#^Variable \$result might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/media.php + - + message: '#^Variable \$network_current might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/plugin.php + - + message: '#^Variable \$metakey might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/includes/post.php + - + message: '#^Variable \$revision might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/includes/revision.php + - + message: '#^Variable \$required_mysql_version might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/includes/update-core.php + - + message: '#^Variable \$required_php_version might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/includes/update-core.php + - + message: '#^Variable \$working_dir_local might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/includes/update-core.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 9 + path: ../../../src/wp-admin/includes/update-core.php + - + message: '#^Variable \$id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/includes/upgrade.php + - + message: '#^Variable \$compat might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/install.php + - + message: '#^Variable \$linkcheck might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/link.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 9 + path: ../../../src/wp-admin/maint/repair.php + - + message: '#^Variable \$update_data might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/menu.php + - + message: '#^Variable \$current_user might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/my-sites.php + - + message: '#^Variable \$current_user might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/nav-menus.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/network.php + - + message: '#^Variable \$current_blog might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/network/admin.php + - + message: '#^Variable \$current_site might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/network/admin.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/network/site-new.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: ../../../src/wp-admin/network/site-settings.php + - + message: '#^Variable \$update might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/network/site-users.php + - + message: '#^Variable \$s might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/network/sites.php + - + message: '#^Variable \$page might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/network/themes.php + - + message: '#^Variable \$status might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/network/themes.php + - + message: '#^Variable \$usersearch might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/network/users.php + - + message: '#^Variable \$user_email might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/options-discussion.php + - + message: '#^Variable \$is_caddy might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/options-permalink.php + - + message: '#^Variable \$is_nginx might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/options-permalink.php + - + message: '#^Variable \$wp_rewrite might not be defined\.$#' + identifier: variable.undefined + count: 8 + path: ../../../src/wp-admin/options-permalink.php + - + message: '#^Variable \$options_to_update might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/options.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/options.php + - + message: '#^Variable \$page might not be defined\.$#' + identifier: variable.undefined + count: 18 + path: ../../../src/wp-admin/plugins.php + - + message: '#^Variable \$plugins might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/plugins.php + - + message: '#^Variable \$status might not be defined\.$#' + identifier: variable.undefined + count: 18 + path: ../../../src/wp-admin/plugins.php + - + message: '#^Variable \$user_ID might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/plugins.php + - + message: '#^Variable \$typenow might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/post.php + - + message: '#^Variable \$h1 might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/revision.php + - + message: '#^Variable \$return_to_post might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/revision.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: ../../../src/wp-admin/setup-config.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/site-editor.php + - + message: '#^Variable \$taxnow might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/term.php + - + message: '#^Variable \$docs_select might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/theme-editor.php + - + message: '#^Variable \$paged might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/theme-install.php + - + message: '#^Variable \$self might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/themes.php + - + message: '#^Variable \$submenu might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/themes.php + - + message: '#^Variable \$auto_update_notice might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/update-core.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/update-core.php + - + message: '#^Variable \$message might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/upgrade.php + - + message: '#^Variable \$version_url might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/upgrade.php + - + message: '#^Variable \$wp_db_version might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/upgrade.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/upload.php + - + message: '#^Variable \$_wp_admin_css_colors might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/user-edit.php + - + message: '#^Variable \$user_login might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/user-edit.php + - + message: '#^Variable \$wp_roles might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/user-edit.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 12 + path: ../../../src/wp-admin/user-edit.php + - + message: '#^Variable \$blog_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/user-new.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/user-new.php + - + message: '#^Variable \$current_blog might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/user/admin.php + - + message: '#^Variable \$current_site might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/user/admin.php + - + message: '#^Variable \$blog_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/users.php + - + message: '#^Variable \$current_user might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: ../../../src/wp-admin/users.php + - + message: '#^Variable \$redirect might not be defined\.$#' + identifier: variable.undefined + count: 10 + path: ../../../src/wp-admin/users.php + - + message: '#^Variable \$usersearch might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/users.php + - + message: '#^Variable \$wp_roles might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/users.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: ../../../src/wp-admin/users.php + - + message: '#^Variable \$title might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/widgets-form-blocks.php + - + message: '#^Variable \$control might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: ../../../src/wp-admin/widgets-form.php + - + message: '#^Variable \$control_callback might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/widgets-form.php + - + message: '#^Variable \$title might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-admin/widgets-form.php + - + message: '#^Variable \$wp_registered_sidebars might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/widgets-form.php + - + message: '#^Variable \$wp_registered_widget_controls might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/widgets-form.php + - + message: '#^Variable \$wp_registered_widget_updates might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-admin/widgets-form.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-content/themes/twentyeleven/header.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: ../../../src/wp-content/themes/twentyeleven/image.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-content/themes/twentyeleven/showcase.php + - + message: '#^Variable \$next_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-content/themes/twentyfourteen/functions.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-content/themes/twentyfourteen/image.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page-panels.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-content/themes/twentyseventeen/template-parts/page/content-front-page.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-content/themes/twentyten/header.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-content/themes/twentyten/loop-attachment.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-content/themes/twentyten/loop.php + - + message: '#^Variable \$wp_query might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-content/themes/twentyten/loop.php + - + message: '#^Variable \$next_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-content/themes/twentythirteen/functions.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-content/themes/twentythirteen/image.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 6 + path: ../../../src/wp-content/themes/twentytwelve/image.php + - + message: '#^Variable \$comments might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-content/themes/twentytwenty/comments.php + - + message: '#^Variable \$post_meta might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-content/themes/twentytwenty/inc/template-tags.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: ../../../src/wp-content/themes/twentytwentyone/image.php + - + message: '#^Variable \$wp_query might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-content/themes/twentytwentyone/search.php + - + message: '#^Variable \$original_content might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/blocks.php + - + message: '#^Variable \$name might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-walker-category.php + - + message: '#^Variable \$comment_closing_at might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$comment_opening_at might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$has_closer might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$has_void_flag might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$json_at might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$json_length might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$name_at might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$name_length might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$namespace_at might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: ../../../src/wp-includes/class-wp-block-processor.php + - + message: '#^Variable \$section_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-customize-widgets.php + - + message: '#^Variable \$filter_id might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-duotone.php + - + message: '#^Variable \$processed_response might not be defined\.$#' + identifier: variable.undefined + count: 5 + path: ../../../src/wp-includes/class-wp-http-streams.php + - + message: '#^Variable \$response might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-http-streams.php + - + message: '#^Variable \$result might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-image-editor.php + - + message: '#^Variable \$network might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-network.php + - + message: '#^Variable \$cache_found might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-query.php + - + message: '#^Variable \$cache_key might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-includes/class-wp-query.php + - + message: '#^Variable \$post_type_cap might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-includes/class-wp-query.php + - + message: '#^Variable \$end might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-includes/class-wp-walker.php + - + message: '#^Variable \$matches might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp.php + - + message: '#^Variable \$query might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp.php + - + message: '#^Variable \$_comments might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-includes/comment-template.php + - + message: '#^Variable \$post_author might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/comment.php + - + message: '#^Variable \$comment might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: ../../../src/wp-includes/feed-atom-comments.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/feed-rdf.php + - + message: '#^Variable \$comment might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-includes/feed-rss2-comments.php + - + message: '#^Variable \$mime might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/functions.php + - + message: '#^Variable \$gen might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/general-template.php + - + message: '#^Variable \$my_year might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/general-template.php + - + message: '#^Variable \$cache_key might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/global-styles-and-settings.php + - + message: '#^Variable \$cached might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/global-styles-and-settings.php + - + message: '#^Variable \$token_length might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/html-api/class-wp-html-decoder.php + - + message: '#^Variable \$node might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^Variable \$parts might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/kses.php + - + message: '#^Variable \$post might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-includes/link-template.php + - + message: '#^Variable \$current_blog might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-includes/ms-files.php + - + message: '#^Variable \$current_blog might not be defined\.$#' + identifier: variable.undefined + count: 7 + path: ../../../src/wp-includes/ms-settings.php + - + message: '#^Variable \$table_prefix might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/ms-settings.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-includes/ms-settings.php + - + message: '#^Variable \$icon might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/post.php + - + message: '#^Variable \$wp_version might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-includes/script-loader.php + - + message: '#^Variable \$manage_url might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/user.php + - + message: '#^Variable \$error might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-login.php + - + message: '#^Variable \$rp_key might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-login.php + - + message: '#^Variable \$rp_login might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-login.php + - + message: '#^Variable \$user_login might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-login.php + - + message: '#^Variable \$subject might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-mail.php + - + message: '#^Variable \$table_prefix might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-settings.php + - + message: '#^Variable \$wp_filter might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-settings.php + - + message: '#^Variable \$wp_query might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-signup.php + - + message: '#^Variable \$posts might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-trackback.php + - + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: ../../../src/wp-trackback.php From b46a011d0df352ab829a43099bb2e99447e13291 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 16:05:55 -0700 Subject: [PATCH 37/51] Add a composer script to regenerate the baselines per identifier PHPStan's --generate-baseline captures every error a run reports, with no way to restrict it to one identifier. That makes it unusable for refreshing a single per-identifier baseline: rerunning it would sweep every other kind of error into the same file. The header of the file added in the previous commit documented a raw --generate-baseline invocation, which was wrong for exactly that reason, and which also omitted the -c and so would have run against a local phpstan.neon at whatever level that sets. Add tests/phpstan/generate-baselines.php, wired up as `composer phpstan:baselines`. It copies the configuration with the `includes` of the baseline directory removed, so those errors surface again, runs the analysis, and splits the result by identifier into one file each. Naming an identifier that no longer reports anything deletes its file rather than leaving an empty one behind, which is the intended end state for each of them. The intermediate baseline is generated in PHPStan's PHP format and read with `require`, so the entries arrive as an array. Nothing parses, or re-escapes, the message patterns. Both temporary files are written beside the configuration rather than to the system temporary directory. A neon file's `includes` resolve relative to its own directory, and PHPStan writes a PHP baseline's paths as __DIR__ followed by a relative chain that it can only produce when the baseline shares an ancestry with the files it names. Generated elsewhere it emits `__DIR__ . '//absolute'` instead, and every path resolves under the temporary directory. PHPStan reports on stdout, which --combined reserves for the baseline, so its output is redirected to stderr. It stays visible on a terminal and stdout is parseable when redirected. Verified by regenerating the committed baseline. All 192 entries come back byte for byte identical to the ones PHPStan generated, so the NEON written here matches what PHPStan writes itself, and the only change to the file is its header. Re-running the analysis afterwards reports no errors and no unmatched ignores. Co-Authored-By: Claude Opus 5 (1M context) --- composer.json | 1 + .../phpstan/baselines/variable.undefined.neon | 15 +- tests/phpstan/generate-baselines.php | 515 ++++++++++++++++++ 3 files changed, 521 insertions(+), 10 deletions(-) create mode 100644 tests/phpstan/generate-baselines.php diff --git a/composer.json b/composer.json index 1bff1b4d62dd7..a04cdedd18d84 100644 --- a/composer.json +++ b/composer.json @@ -68,6 +68,7 @@ }, "scripts": { "phpstan": "@php ./vendor/bin/phpstan analyse --memory-limit=2G", + "phpstan:baselines": [ "Composer\\Config::disableProcessTimeout", "@php ./tests/phpstan/generate-baselines.php" ], "compat": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcompat.xml.dist --report=summary,source", "format": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf --report=summary,source", "lint": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --report=summary,source", diff --git a/tests/phpstan/baselines/variable.undefined.neon b/tests/phpstan/baselines/variable.undefined.neon index b2a361a9df34c..9b52c9a97ea49 100644 --- a/tests/phpstan/baselines/variable.undefined.neon +++ b/tests/phpstan/baselines/variable.undefined.neon @@ -1,9 +1,6 @@ -# PHPStan baseline for the level 1 `variable.undefined` errors in WordPress core. +# PHPStan baseline for the `variable.undefined` errors in WordPress core. # -# Every entry in this file is the same error: a variable -- nearly always a -# WordPress global such as $wpdb, $wp_query or $post -- is read in a file that -# never declares it with a `global` statement, so the analysis cannot see where -# it comes from. +# https://phpstan.org/error-identifiers/variable.undefined # # 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 @@ -13,13 +10,11 @@ # The goal is to empty this file and delete it, along with the `includes` entry # for it in phpstan.neon.dist. # -# To regenerate rather than hand-edit, temporarily remove that `includes` entry -# so the errors surface again, then run: +# Generated by `composer phpstan:baselines`. Do not edit by hand; regenerate with # -# vendor/bin/phpstan analyse \ -# --generate-baseline=tests/phpstan/baselines/variable.undefined.neon +# composer phpstan:baselines -- --identifier=variable.undefined # -# and restore this header afterwards. +# which reruns the analysis with this file suppressed so the errors surface again. parameters: ignoreErrors: diff --git a/tests/phpstan/generate-baselines.php b/tests/phpstan/generate-baselines.php new file mode 100644 index 0000000000000..7905ec522e135 --- /dev/null +++ b/tests/phpstan/generate-baselines.php @@ -0,0 +1,515 @@ + all-errors.neon + * + * @package WordPress + */ + +namespace WordPress\PHPStan; + +if ( 'cli' !== PHP_SAPI ) { + fwrite( STDERR, "This script must be run from the command line.\n" ); + exit( 1 ); +} + +$repo_root = dirname( __DIR__, 2 ); + +// $argv is only populated when register_argc_argv is on, so read it defensively. +$args = array(); +foreach ( (array) ( $_SERVER['argv'] ?? array() ) as $arg ) { + if ( is_string( $arg ) ) { + $args[] = $arg; + } +} +array_shift( $args ); + +$config_option = 'phpstan.neon.dist'; +$output_option = 'tests/phpstan/baselines'; +$memory_limit = '2G'; +$only_identifiers = array(); +$combined = false; + +foreach ( $args as $arg ) { + if ( '--help' === $arg || '-h' === $arg ) { + fwrite( STDOUT, get_usage() ); + exit( 0 ); + } + + if ( '--combined' === $arg ) { + $combined = true; + continue; + } + + if ( 1 === preg_match( '/^--identifier=(.+)$/', $arg, $matches ) ) { + foreach ( explode( ',', $matches[1] ) as $identifier ) { + $identifier = trim( $identifier ); + if ( '' !== $identifier ) { + $only_identifiers[] = $identifier; + } + } + continue; + } + + if ( 1 === preg_match( '/^--config=(.+)$/', $arg, $matches ) ) { + $config_option = $matches[1]; + continue; + } + + if ( 1 === preg_match( '/^--output-dir=(.+)$/', $arg, $matches ) ) { + $output_option = $matches[1]; + continue; + } + + if ( 1 === preg_match( '/^--memory-limit=(.+)$/', $arg, $matches ) ) { + $memory_limit = $matches[1]; + continue; + } + + fwrite( STDERR, "Unrecognized option: $arg\n\n" . get_usage() ); + exit( 1 ); +} + +$config_path = $repo_root . '/' . ltrim( $config_option, '/' ); +$output_dir = $repo_root . '/' . trim( $output_option, '/' ); + +if ( ! is_file( $config_path ) ) { + fwrite( STDERR, "Configuration not found: $config_option\n" ); + exit( 1 ); +} + +/* + * The temporary configuration has to sit beside the original, because a neon + * file's `includes` entries resolve relative to its own directory. + */ +/* + * Both temporary files sit beside the configuration, and so inside the + * repository, for two separate reasons. + * + * A neon file's `includes` resolve relative to its own directory, so the copy of + * the configuration has to live where the original did. + * + * PHPStan writes a PHP baseline's paths as __DIR__ followed by a relative chain, + * which it can only produce when the baseline shares an ancestry with the files + * it names. Generated somewhere else, the system temporary directory included, + * it emits `__DIR__ . '//absolute/path'` instead, and every path in it then + * resolves to somewhere under that directory rather than to the source file. + */ +$temp_config = dirname( $config_path ) . '/.phpstan-baselines-' . getmypid() . '.neon'; +$temp_baseline = dirname( $config_path ) . '/.phpstan-baselines-' . getmypid() . '.php'; + +register_shutdown_function( + static function () use ( $temp_config, $temp_baseline ): void { + foreach ( array( $temp_config, $temp_baseline ) as $file ) { + if ( is_file( $file ) ) { + unlink( $file ); + } + } + } +); + +file_put_contents( $temp_config, strip_baseline_includes( $config_path, $output_dir ) ); + +/* + * PHPStan reports on stdout, which --combined reserves for the baseline itself, + * so its output is sent to stderr. That keeps it visible on a terminal while + * leaving stdout parseable when it is redirected. + */ +$command = sprintf( + '%s analyse --configuration=%s --generate-baseline=%s --allow-empty-baseline --no-progress --memory-limit=%s 1>&2', + escapeshellarg( $repo_root . '/vendor/bin/phpstan' ), + escapeshellarg( $temp_config ), + escapeshellarg( $temp_baseline ), + escapeshellarg( $memory_limit ) +); + +fwrite( STDERR, "Analyzing with $config_option, existing baselines suppressed...\n" ); + +$exit_code = 0; +passthru( $command, $exit_code ); + +if ( 0 !== $exit_code || ! is_file( $temp_baseline ) ) { + fwrite( STDERR, "PHPStan failed, nothing written.\n" ); + exit( 1 ); +} + +/** + * The entries of each error, grouped by the identifier of the error it suppresses. + * + * @var array, path: non-empty-string}>> $grouped + */ +$grouped = array(); + +foreach ( read_baseline( $temp_baseline ) as $entry ) { + $grouped[ $entry['identifier'] ][] = $entry; +} +ksort( $grouped ); + +if ( $only_identifiers ) { + $grouped = array_intersect_key( $grouped, array_flip( $only_identifiers ) ); +} + +if ( $combined ) { + $all = array(); + foreach ( $grouped as $entries ) { + $all = array_merge( $all, $entries ); + } + echo build_baseline( $all, $output_dir, "# Every identifier, combined.\n" ); + exit( 0 ); +} + +if ( ! is_dir( $output_dir ) && ! mkdir( $output_dir, 0755, true ) ) { + fwrite( STDERR, "Could not create $output_option\n" ); + exit( 1 ); +} + +foreach ( $grouped as $identifier => $entries ) { + file_put_contents( + $output_dir . '/' . $identifier . '.neon', + build_baseline( $entries, $output_dir, build_baseline_header( $identifier, $config_option ) ) + ); + + printf( + "%s: %d entries, %d errors\n", + $output_option . '/' . $identifier . '.neon', + count( $entries ), + count_errors( $entries ) + ); +} + +/* + * An identifier that was asked for but reported nothing has been driven to zero, + * so retire its file rather than leaving an empty one behind. + */ +foreach ( $only_identifiers as $identifier ) { + if ( isset( $grouped[ $identifier ] ) ) { + continue; + } + + $file = $output_dir . '/' . $identifier . '.neon'; + if ( is_file( $file ) && unlink( $file ) ) { + printf( + "%s: no errors remain, file deleted. Remove its `includes` entry from %s.\n", + $output_option . '/' . $identifier . '.neon', + $config_option + ); + } else { + printf( "%s: no errors reported.\n", $identifier ); + } +} + +/** + * Returns the usage message. + * + * @return non-falsy-string Usage message. + */ +function get_usage(): string { + return <<<'TEXT' + Generates PHPStan baselines split by error identifier. + + Usage: + composer phpstan:baselines [-- ] + + Options: + --identifier= Only write the baseline for this identifier. Repeatable, + or comma separated. When an identifier is named and the + analysis reports none of it, its baseline file is deleted + rather than left behind empty. + Default: every identifier reported. + --config= Configuration to analyze with, relative to the repository + root. Default: phpstan.neon.dist + --output-dir= Where the per-identifier baselines are written, relative + to the repository root. Paths inside them are written + relative to this directory. + Default: tests/phpstan/baselines + --combined Print one combined baseline to stdout instead of writing + per-identifier files. Nothing is written to disk. + --memory-limit= Passed through to PHPStan. Default: 2G + -h, --help Show this message. + + Examples: + Refresh every baseline: + composer phpstan:baselines + + Refresh one: + composer phpstan:baselines -- --identifier=variable.undefined + + Refresh several, either comma separated or by repeating the option: + composer phpstan:baselines -- --identifier=variable.undefined,isset.variable + composer phpstan:baselines -- --identifier=isset.variable --identifier=empty.variable + + Inspect everything as one baseline without writing any files: + composer phpstan:baselines -- --combined + + TEXT; +} + +/** + * Reads a file, failing loudly rather than continuing with false. + * + * @param non-falsy-string $path Absolute path to the file. + * @return string File contents. + */ +function read_file( string $path ): string { + $contents = file_get_contents( $path ); + + if ( false === $contents ) { + fwrite( STDERR, "Could not read $path\n" ); + exit( 1 ); + } + + return $contents; +} + +/** + * Reads a baseline generated in PHPStan's PHP format. + * + * The file returns the entries as an array, so it is required rather than + * parsed. Its `path` values are built from __DIR__ and so arrive absolute. + * + * @param non-falsy-string $path Absolute path to the generated baseline. + * @return list, path: non-empty-string}> Baseline entries. + */ +function read_baseline( string $path ): array { + $data = require $path; + + $parameters = is_array( $data ) ? ( $data['parameters'] ?? null ) : null; + $ignore_errors = is_array( $parameters ) ? ( $parameters['ignoreErrors'] ?? null ) : null; + + if ( ! is_array( $ignore_errors ) ) { + fwrite( STDERR, "Unexpected baseline structure in $path\n" ); + exit( 1 ); + } + + $entries = array(); + + foreach ( $ignore_errors as $entry ) { + if ( ! is_array( $entry ) + || ! isset( $entry['message'], $entry['identifier'], $entry['count'], $entry['path'] ) + || ! is_string( $entry['message'] ) + || ! is_string( $entry['identifier'] ) + || ! is_int( $entry['count'] ) + || $entry['count'] < 0 + || ! is_string( $entry['path'] ) + || '' === $entry['path'] + ) { + fwrite( STDERR, "Unexpected baseline entry in $path.\n" ); + exit( 1 ); + } + + /* + * PHPStan attaches an identifier to every error it reports, so an entry + * without a usable one means this is not a baseline that can be split by + * identifier. Skipping it would quietly drop a suppression. + */ + if ( '' === $entry['identifier'] || '0' === $entry['identifier'] ) { + fwrite( STDERR, "Baseline entry in $path has no identifier.\n" ); + exit( 1 ); + } + + $entries[] = array( + 'message' => $entry['message'], + 'identifier' => $entry['identifier'], + 'count' => $entry['count'], + 'path' => $entry['path'], + ); + } + + return $entries; +} + +/** + * Returns the configuration with any `includes` of the baseline directory removed. + * + * Those files suppress the very errors being regenerated, so they have to be out + * of the way for the analysis to report anything. + * + * @param non-falsy-string $config_path Absolute path to the configuration file. + * @param non-falsy-string $output_dir Absolute path to the baseline directory. + * @return string Configuration contents. + */ +function strip_baseline_includes( string $config_path, string $output_dir ): string { + $config_dir = dirname( $config_path ); + $in_block = false; + $kept = array(); + + foreach ( explode( "\n", read_file( $config_path ) ) as $line ) { + if ( 1 === preg_match( '/^includes:/', $line ) ) { + $in_block = true; + $kept[] = $line; + continue; + } + + // A non-indented, non-blank line ends the block. + if ( $in_block && '' !== trim( $line ) && 1 !== preg_match( '/^\s/', $line ) ) { + $in_block = false; + } + + if ( $in_block && 1 === preg_match( '/^\s*-\s*(\S+)\s*$/', $line, $matches ) ) { + $included = $matches[1]; + $absolute = ( '/' === $included[0] ) ? $included : $config_dir . '/' . $included; + + if ( 0 === strpos( normalize_path( $absolute ), normalize_path( $output_dir ) . '/' ) ) { + continue; + } + } + + $kept[] = $line; + } + + return implode( "\n", $kept ); +} + +/** + * Resolves ".." segments in a path without requiring it to exist. + * + * @param non-empty-string $path Path to normalize. + * @return non-falsy-string Normalized path, always absolute. + */ +function normalize_path( string $path ): string { + $parts = array(); + + foreach ( explode( '/', $path ) as $part ) { + if ( '' === $part || '.' === $part ) { + continue; + } + if ( '..' === $part ) { + array_pop( $parts ); + continue; + } + $parts[] = $part; + } + + return '/' . implode( '/', $parts ); +} + +/** + * Expresses one absolute path relative to a directory. + * + * A result of "0" is possible in principle, when the target is a single segment + * named "0" directly inside $from_dir, so this is non-empty rather than non-falsy. + * + * @param non-empty-string $from_dir Directory to express the path relative to. + * @param non-empty-string $to_path Path to express. + * @return non-empty-string Relative path, or "." when the two are the same. + */ +function get_relative_path( string $from_dir, string $to_path ): string { + $from = explode( '/', trim( normalize_path( $from_dir ), '/' ) ); + $to = explode( '/', trim( normalize_path( $to_path ), '/' ) ); + + while ( $from && $to && $from[0] === $to[0] ) { + array_shift( $from ); + array_shift( $to ); + } + + $relative = str_repeat( '../', count( $from ) ) . implode( '/', $to ); + + return '' === $relative ? '.' : $relative; +} + +/** + * Totals the `count` values across a set of entries. + * + * @param list, path: non-empty-string}> $entries Baseline entries. + * @return int<0, max> Total number of errors. + */ +function count_errors( array $entries ): int { + $total = 0; + + foreach ( $entries as $entry ) { + $total += $entry['count']; + } + + return $total; +} + +/** + * Builds a baseline file in PHPStan's NEON format. + * + * The entry layout matches what PHPStan itself writes, so a regenerated file can + * be diffed against one it produced. Paths are rewritten relative to the file's + * own directory, since that is what a NEON `path` resolves against. + * + * @param list, path: non-empty-string}> $entries Baseline entries. + * @param non-empty-string $output_dir Directory the file is written to. + * @param string $header Comment block, or an empty string. + * @return non-falsy-string Baseline file contents. + */ +function build_baseline( array $entries, string $output_dir, string $header ): string { + $contents = ( '' === $header ? '' : $header . "\n" ) . "parameters:\n\tignoreErrors:\n"; + + foreach ( $entries as $entry ) { + $contents .= "\t\t-\n" + . "\t\t\tmessage: " . quote_neon_value( $entry['message'] ) . "\n" + . "\t\t\tidentifier: " . $entry['identifier'] . "\n" + . "\t\t\tcount: " . $entry['count'] . "\n" + . "\t\t\tpath: " . get_relative_path( $output_dir, $entry['path'] ) . "\n"; + } + + return $contents; +} + +/** + * Quotes a value for NEON. + * + * A single quoted NEON string has no escape sequences other than a doubled + * quote, so the backslashes in a message pattern survive as written. This is the + * same quoting PHPStan applies when it generates a baseline itself. + * + * @param string $value Value to quote. + * @return non-falsy-string Quoted value. + */ +function quote_neon_value( string $value ): string { + return "'" . str_replace( "'", "''", $value ) . "'"; +} + +/** + * Builds the header comment for a per-identifier baseline. + * + * @param non-falsy-string $identifier Error identifier, a group followed by a code. + * @param non-empty-string $config Configuration path, as passed on the command line. + * @return non-falsy-string Comment block. + */ +function build_baseline_header( string $identifier, string $config ): string { + return << Date: Tue, 4 Aug 2026 20:15:15 -0700 Subject: [PATCH 38/51] Revert PHPStan level 1 fixes --- src/wp-admin/admin.php | 2 +- .../includes/class-custom-image-header.php | 2 +- .../includes/class-wp-posts-list-table.php | 16 +++++++++++++++- src/wp-admin/includes/file.php | 11 ++++++----- src/wp-admin/includes/plugin.php | 18 ++++++++++-------- src/wp-includes/class-wp-xmlrpc-server.php | 1 - src/wp-includes/class-wpdb.php | 2 -- src/wp-includes/template.php | 4 +--- src/wp-includes/theme-compat/embed-content.php | 1 - src/wp-includes/user.php | 5 ++--- 10 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php index 39ea85470a265..82ab6b93ac99e 100644 --- a/src/wp-admin/admin.php +++ b/src/wp-admin/admin.php @@ -210,7 +210,7 @@ $hook_suffix = $page_hook; } elseif ( isset( $plugin_page ) ) { $hook_suffix = $plugin_page; -} else { +} elseif ( isset( $pagenow ) ) { $hook_suffix = $pagenow; } diff --git a/src/wp-admin/includes/class-custom-image-header.php b/src/wp-admin/includes/class-custom-image-header.php index c1816ffae2d9c..97a1a16052d12 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]; - } else { + } elseif ( isset( $_POST ) ) { $data = $this->step_2_manage_upload(); $attachment_id = $data['attachment_id']; $file = $data['file']; diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index 575f4921e6ba2..8a319986766b8 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -298,6 +298,7 @@ protected function get_views() { $status_links = array(); $num_posts = wp_count_posts( $post_type, 'readable' ); $total_posts = array_sum( (array) $num_posts ); + $class = ''; $current_user_id = get_current_user_id(); $all_args = array( 'post_type' => $post_type ); @@ -309,6 +310,10 @@ protected function get_views() { } if ( $this->user_posts_count && $this->user_posts_count !== $total_posts ) { + if ( isset( $_GET['author'] ) && ( $current_user_id === (int) $_GET['author'] ) ) { + $class = 'current'; + } + $mine_args = array( 'post_type' => $post_type, 'author' => $current_user_id, @@ -332,6 +337,7 @@ protected function get_views() { ); $all_args['all_posts'] = 1; + $class = ''; } $all_inner_html = sprintf( @@ -348,7 +354,7 @@ protected function get_views() { $status_links['all'] = array( 'url' => esc_url( add_query_arg( $all_args, 'edit.php' ) ), 'label' => $all_inner_html, - 'current' => ( $this->is_base_request() || isset( $_REQUEST['all_posts'] ) ), + 'current' => empty( $class ) && ( $this->is_base_request() || isset( $_REQUEST['all_posts'] ) ), ); if ( $mine ) { @@ -356,12 +362,18 @@ protected function get_views() { } foreach ( get_post_stati( array( 'show_in_admin_status_list' => true ), 'objects' ) as $status ) { + $class = ''; + $status_name = $status->name; if ( ! in_array( $status_name, $avail_post_stati, true ) || empty( $num_posts->$status_name ) ) { continue; } + if ( isset( $_REQUEST['post_status'] ) && $status_name === $_REQUEST['post_status'] ) { + $class = 'current'; + } + $status_args = array( 'post_status' => $status_name, 'post_type' => $post_type, @@ -380,6 +392,8 @@ protected function get_views() { } if ( ! empty( $this->sticky_posts_count ) ) { + $class = ! empty( $_REQUEST['show_sticky'] ) ? 'current' : ''; + $sticky_args = array( 'post_type' => $post_type, 'show_sticky' => 1, diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 4f0cdef6ad454..8c0015020f35d 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -397,10 +397,9 @@ function wp_edit_theme_plugin_file( $args ) { $file = $args['file']; $content = $args['newcontent']; - $plugin = null; - $theme = null; - $stylesheet = null; - $real_file = null; + $plugin = null; + $theme = null; + $real_file = null; if ( ! empty( $args['plugin'] ) ) { $plugin = $args['plugin']; @@ -561,7 +560,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' ) ); - } else { + } elseif ( isset( $stylesheet ) ) { $url = add_query_arg( array( 'theme' => $stylesheet, @@ -569,6 +568,8 @@ function wp_edit_theme_plugin_file( $args ) { ), admin_url( 'theme-editor.php' ) ); + } else { + $url = admin_url(); } if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) { diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index a8d83ff62374e..94d4a585a80f1 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2089,14 +2089,16 @@ function get_admin_page_title() { } } } - foreach ( $menu as $menu_array ) { - if ( isset( $plugin_page ) - && $plugin_page === $menu_array[2] - && 'admin.php' === $pagenow - && $parent1 === $menu_array[2] - ) { - $title = $menu_array[3]; - return $menu_array[3]; + if ( empty( $title ) ) { + foreach ( $menu as $menu_array ) { + if ( isset( $plugin_page ) + && $plugin_page === $menu_array[2] + && 'admin.php' === $pagenow + && $parent1 === $menu_array[2] + ) { + $title = $menu_array[3]; + return $menu_array[3]; + } } } } diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index d1ea40c8d714c..1061dbd1831d2 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -7105,7 +7105,6 @@ public function pingback_ping( $args ) { $p = explode( "\n\n", $remote_source ); - $excerpt = ''; $preg_target = preg_quote( $pagelinkedto, '|' ); foreach ( $p as $para ) { diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index 1598484ade9d1..e9d7f986d5801 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -2120,8 +2120,6 @@ public function parse_db_host( $host ) { * * @param bool $allow_bail Optional. Allows the function to bail. Default true. * @return bool Whether the connection is up. Exits if down and $allow_bail is true. - * - * @phpstan-return ( $allow_bail is true ? bool|never : bool ) */ public function check_connection( $allow_bail = true ) { // Check if the connection is alive. diff --git a/src/wp-includes/template.php b/src/wp-includes/template.php index ae22a76050aac..6ec1934f866ec 100644 --- a/src/wp-includes/template.php +++ b/src/wp-includes/template.php @@ -722,10 +722,8 @@ function wp_set_template_globals() { function locate_template( $template_names, $load = false, $load_once = true, $args = array() ) { global $wp_stylesheet_path, $wp_template_path; - /** @see wp_set_template_globals() */ if ( ! isset( $wp_stylesheet_path ) || ! isset( $wp_template_path ) ) { - $wp_stylesheet_path = get_stylesheet_directory(); - $wp_template_path = get_template_directory(); + wp_set_template_globals(); } $is_child_theme = is_child_theme(); diff --git a/src/wp-includes/theme-compat/embed-content.php b/src/wp-includes/theme-compat/embed-content.php index 8bf961ba2dace..42884fec0c5bc 100644 --- a/src/wp-includes/theme-compat/embed-content.php +++ b/src/wp-includes/theme-compat/embed-content.php @@ -13,7 +13,6 @@
> user_pass; } else { - $update = false; - $old_user_data = null; + $update = false; if ( empty( $userdata['user_pass'] ) ) { wp_trigger_error( @@ -2398,7 +2397,7 @@ function wp_insert_user( $userdata ) { * check if current email and new email are the same, and check `email_exists` * accordingly. */ - if ( ( ! $update || 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ) + if ( ( ! $update || ( ! empty( $old_user_data ) && 0 !== strcasecmp( $user_email, $old_user_data->user_email ) ) ) && ! defined( 'WP_IMPORTING' ) && email_exists( $user_email ) ) { From 5d6e3492a2419766fda6115e039a0e40b98c4469 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 20:48:52 -0700 Subject: [PATCH 39/51] Manage the baseline includes and retire empty baselines automatically Splitting the baselines by identifier left two things to be done by hand. A newly split out baseline had to be added to the `includes` in phpstan.neon.dist, and one whose identifier had reached zero had to be removed from it. Forgetting the first means the baseline is written but never read, so its errors are reported anyway. Forgetting the second means PHPStan is pointed at a file that is gone. Have `composer phpstan:baselines` do both. The list of baselines now sits between `# phpstan:baselines` markers in the `includes`, and is rewritten after each run to match what is on disk, so the hand written entries around it are never touched. Where the markers are absent they are appended to the end of the block, which is what happens the first time this runs against a configuration. A baseline whose identifier no longer reports anything is also deleted. This previously happened only for an identifier named with --identifier, which left a full run unable to retire anything: the stale file stayed, and its entries were then reported as unmatched ignores. A full run has seen every identifier, so it may retire any file that no longer corresponds to one. Fold tests/phpstan/baseline.php into the split baselines and delete it. It held ten entries, six `empty.variable` and four `isset.variable`, which is to say nothing but the two identifiers that now have their own files. Keeping it would mean a second, hand maintained suppression mechanism alongside the generated ones, and the one place where the exact counts that make these files a tripwire do not apply. Every suppression is now generated, and is now in one place. Verified by regenerating from scratch, which reports no errors and no unmatched ignores. The `includes` rewriting was exercised by removing two entries and adding a stale one, and the retiring by leaving a baseline for an identifier that reports nothing; the run restored both entries, dropped the stale one, and deleted the file. Co-Authored-By: Claude Opus 5 (1M context) --- phpstan.neon.dist | 15 +- tests/phpstan/baseline.php | 65 ------- tests/phpstan/baselines/empty.variable.neon | 65 +++++++ tests/phpstan/baselines/isset.variable.neon | 50 ++++++ .../phpstan/baselines/variable.undefined.neon | 15 ++ tests/phpstan/generate-baselines.php | 163 +++++++++++++++++- 6 files changed, 295 insertions(+), 78 deletions(-) delete mode 100644 tests/phpstan/baseline.php create mode 100644 tests/phpstan/baselines/empty.variable.neon create mode 100644 tests/phpstan/baselines/isset.variable.neon diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 18d5dd9bf0fab..3f56518228eed 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -14,13 +14,18 @@ includes: # new strict rules. - vendor/phpstan/phpstan-phpunit/extension.neon - # The baseline file includes preexisting errors in the codebase that should be ignored. + # Preexisting errors that should be ignored, one baseline per error identifier + # so that the remaining work on each is visible as a single shrinking file. + # Each is meant to reach zero and be deleted, taking its line below with it. # https://phpstan.org/user-guide/baseline - - tests/phpstan/baseline.php - - # The level 1 variable.undefined errors, kept separate so that the remaining - # work on them is visible as a single shrinking file. See its header. + # + # Regenerate with `composer phpstan:baselines`, which rewrites both the files + # and the list between the markers. Do not edit that list by hand. + # phpstan:baselines start + - tests/phpstan/baselines/empty.variable.neon + - tests/phpstan/baselines/isset.variable.neon - tests/phpstan/baselines/variable.undefined.neon + # phpstan:baselines end parameters: # https://phpstan.org/user-guide/rule-levels diff --git a/tests/phpstan/baseline.php b/tests/phpstan/baseline.php deleted file mode 100644 index e6ec48330389d..0000000000000 --- a/tests/phpstan/baseline.php +++ /dev/null @@ -1,65 +0,0 @@ - '#^Variable \\$_POST in isset\\(\\) always exists and is not nullable\\.$#', - 'identifier' => 'isset.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/includes/media.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$parent_file in empty\\(\\) always exists and is not falsy\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-admin/themes.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$addl_path in empty\\(\\) always exists and is always falsy\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/canonical.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$namespace in isset\\(\\) always exists and is not nullable\\.$#', - 'identifier' => 'isset.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/class-wp-block-parser.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$loader in isset\\(\\) always exists and is not nullable\\.$#', - 'identifier' => 'isset.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/class-wp-oembed.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$search in empty\\(\\) always exists and is not falsy\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/class-wp-query.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$status_type_clauses in empty\\(\\) always exists and is not falsy\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/class-wp-query.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$schema in empty\\(\\) is never defined\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$the_parent in empty\\(\\) always exists and is not falsy\\.$#', - 'identifier' => 'empty.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/taxonomy.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Variable \\$s in isset\\(\\) is never defined\\.$#', - 'identifier' => 'isset.variable', - 'count' => 1, - 'path' => __DIR__ . '/../../src/wp-includes/template.php', -]; - -return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; diff --git a/tests/phpstan/baselines/empty.variable.neon b/tests/phpstan/baselines/empty.variable.neon new file mode 100644 index 0000000000000..28186fb9fc850 --- /dev/null +++ b/tests/phpstan/baselines/empty.variable.neon @@ -0,0 +1,65 @@ +# PHPStan baseline for the `empty.variable` errors in WordPress core. +# +# https://phpstan.org/error-identifiers/empty.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=empty.variable +# +# which reruns the analysis with this file suppressed so the errors surface again. + +parameters: + ignoreErrors: + - + message: '#^Variable \$class in empty\(\) always exists and is always falsy\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-admin/includes/class-wp-posts-list-table.php + - + message: '#^Variable \$title in empty\(\) always exists and is always falsy\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-admin/includes/plugin.php + - + message: '#^Variable \$parent_file in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-admin/themes.php + - + message: '#^Variable \$addl_path in empty\(\) always exists and is always falsy\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-includes/canonical.php + - + message: '#^Variable \$search in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-includes/class-wp-query.php + - + message: '#^Variable \$status_type_clauses in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-includes/class-wp-query.php + - + message: '#^Variable \$schema in empty\(\) is never defined\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php + - + message: '#^Variable \$the_parent in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-includes/taxonomy.php + - + message: '#^Variable \$old_user_data in empty\(\) always exists and is not falsy\.$#' + identifier: empty.variable + count: 1 + path: ../../../src/wp-includes/user.php diff --git a/tests/phpstan/baselines/isset.variable.neon b/tests/phpstan/baselines/isset.variable.neon new file mode 100644 index 0000000000000..f14ef0755fdb9 --- /dev/null +++ b/tests/phpstan/baselines/isset.variable.neon @@ -0,0 +1,50 @@ +# 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 \$_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 + 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 + 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 + - + message: '#^Variable \$s in isset\(\) is never defined\.$#' + identifier: isset.variable + count: 1 + path: ../../../src/wp-includes/template.php diff --git a/tests/phpstan/baselines/variable.undefined.neon b/tests/phpstan/baselines/variable.undefined.neon index 9b52c9a97ea49..dca18ebe74acc 100644 --- a/tests/phpstan/baselines/variable.undefined.neon +++ b/tests/phpstan/baselines/variable.undefined.neon @@ -23,6 +23,11 @@ parameters: identifier: variable.undefined count: 1 path: ../../../src/wp-activate.php + - + message: '#^Variable \$pagenow might not be defined\.$#' + identifier: variable.undefined + count: 3 + path: ../../../src/wp-admin/admin.php - message: '#^Variable \$wp_db_version might not be defined\.$#' identifier: variable.undefined @@ -813,6 +818,11 @@ parameters: identifier: variable.undefined count: 2 path: ../../../src/wp-includes/class-wp-walker.php + - + message: '#^Variable \$excerpt might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: ../../../src/wp-includes/class-wp-xmlrpc-server.php - message: '#^Variable \$matches might not be defined\.$#' identifier: variable.undefined @@ -923,6 +933,11 @@ parameters: identifier: variable.undefined count: 2 path: ../../../src/wp-includes/script-loader.php + - + message: '#^Variable \$wp_template_path might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: ../../../src/wp-includes/template.php - message: '#^Variable \$manage_url might not be defined\.$#' identifier: variable.undefined diff --git a/tests/phpstan/generate-baselines.php b/tests/phpstan/generate-baselines.php index 7905ec522e135..de0073e83cc54 100644 --- a/tests/phpstan/generate-baselines.php +++ b/tests/phpstan/generate-baselines.php @@ -12,6 +12,12 @@ * identifier and writes one file per kind. Each file is self-describing and is * meant to shrink to nothing and then be deleted. * + * Baselines whose identifier no longer reports anything are removed, and the + * list of them between the `# phpstan:baselines` markers in the configuration's + * `includes` is rewritten to match what is on disk. Adding a newly split out + * baseline, and retiring one that has reached zero, therefore need no manual + * edit of the configuration. + * * The intermediate baseline is generated in PHPStan's PHP format and read back * with `require`, so the entries arrive as an array. Nothing has to parse, or * re-escape, the message patterns. @@ -199,26 +205,37 @@ static function () use ( $temp_config, $temp_baseline ): void { } /* - * An identifier that was asked for but reported nothing has been driven to zero, - * so retire its file rather than leaving an empty one behind. + * An identifier that reports nothing has been driven to zero, so retire its file + * rather than leaving a stale one behind whose entries would then be reported as + * unmatched ignores. + * + * A run restricted to particular identifiers only knows about those, so it may + * only retire those. A full run has seen everything and may retire any file that + * no longer corresponds to a reported identifier. */ -foreach ( $only_identifiers as $identifier ) { +$retired = $only_identifiers; + +if ( ! $only_identifiers ) { + foreach ( find_baselines( $output_dir ) as $file ) { + $retired[] = basename( $file, '.neon' ); + } +} + +foreach ( $retired as $identifier ) { if ( isset( $grouped[ $identifier ] ) ) { continue; } $file = $output_dir . '/' . $identifier . '.neon'; if ( is_file( $file ) && unlink( $file ) ) { - printf( - "%s: no errors remain, file deleted. Remove its `includes` entry from %s.\n", - $output_option . '/' . $identifier . '.neon', - $config_option - ); + printf( "%s: no errors remain, file deleted.\n", $output_option . '/' . $identifier . '.neon' ); } else { printf( "%s: no errors reported.\n", $identifier ); } } +update_config_includes( $config_path, $config_option, $output_dir ); + /** * Returns the usage message. * @@ -228,6 +245,11 @@ function get_usage(): string { return <<<'TEXT' Generates PHPStan baselines split by error identifier. + Writes one baseline per identifier, retires any whose identifier no longer + reports anything, and rewrites the list of them between the + `# phpstan:baselines` markers in the configuration's `includes`, so that + neither addition nor removal has to be done by hand. + Usage: composer phpstan:baselines [-- ] @@ -381,6 +403,131 @@ function strip_baseline_includes( string $config_path, string $output_dir ): str return implode( "\n", $kept ); } +/** + * Lists the per-identifier baselines present on disk. + * + * @param non-empty-string $output_dir Absolute path to the baseline directory. + * @return list Absolute paths, sorted by name. + */ +function find_baselines( string $output_dir ): array { + $found = glob( $output_dir . '/*.neon' ); + + if ( false === $found ) { + return array(); + } + + sort( $found ); + + $files = array(); + foreach ( $found as $file ) { + if ( '' !== $file ) { + $files[] = $file; + } + } + + return $files; +} + +/** + * Rewrites the managed region of the configuration's `includes` list. + * + * The region is delimited by marker comments, so the hand written entries around + * it are never touched. Where the markers are absent they are appended to the end + * of the `includes` block, which is what happens the first time this is run + * against a configuration. + * + * @param non-falsy-string $config_path Absolute path to the configuration file. + * @param non-empty-string $config_option Configuration path, as passed on the command line. + * @param non-empty-string $output_dir Absolute path to the baseline directory. + */ +function update_config_includes( string $config_path, string $config_option, string $output_dir ): void { + $start_marker = '# phpstan:baselines start'; + $end_marker = '# phpstan:baselines end'; + + $before = read_file( $config_path ); + $lines = explode( "\n", $before ); + + $start = null; + $end = null; + foreach ( $lines as $i => $line ) { + if ( $start_marker === trim( $line ) ) { + $start = $i; + } + if ( $end_marker === trim( $line ) ) { + $end = $i; + } + } + + $region = array( "\t" . $start_marker ); + foreach ( find_baselines( $output_dir ) as $file ) { + $region[] = "\t- " . get_relative_path( dirname( $config_path ), $file ); + } + $region[] = "\t" . $end_marker; + + if ( null !== $start && null !== $end && $start < $end ) { + $updated = array_merge( + array_slice( $lines, 0, $start ), + $region, + array_slice( $lines, $end + 1 ) + ); + } else { + $insert = find_includes_end( $lines ); + + if ( null === $insert ) { + fwrite( STDERR, "No `includes` block found in $config_option, left untouched.\n" ); + return; + } + + $updated = array_merge( + array_slice( $lines, 0, $insert ), + array( '' ), + $region, + array_slice( $lines, $insert ) + ); + } + + $after = implode( "\n", $updated ); + + if ( $before === $after ) { + return; + } + + file_put_contents( $config_path, $after ); + printf( "%s: `includes` updated.\n", $config_option ); +} + +/** + * Finds where the `includes` block ends. + * + * @param list $lines Configuration lines. + * @return int|null Index of the first line after the block, or null when there is none. + */ +function find_includes_end( array $lines ): ?int { + $in_block = false; + $last = null; + + foreach ( $lines as $i => $line ) { + if ( 1 === preg_match( '/^includes:/', $line ) ) { + $in_block = true; + $last = $i; + continue; + } + + if ( ! $in_block || '' === trim( $line ) ) { + continue; + } + + // A non-indented line ends the block. + if ( 1 !== preg_match( '/^\s/', $line ) ) { + break; + } + + $last = $i; + } + + return null === $last ? null : $last + 1; +} + /** * Resolves ".." segments in a path without requiring it to exist. * From 169da583cea3582c963b35844c4f4b738586c1a6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 21:48:20 -0700 Subject: [PATCH 40/51] Docs: Document how the split baselines are regenerated The README still described a single tests/phpstan/baseline.php, regenerated by passing --generate-baseline to PHPStan directly. Both are now wrong, and following the instructions would undo the split: the file no longer exists, and that flag captures every error a run reports, so it would collect every kind of error into whatever file it was pointed at. Describe what is there instead. Two sections cover how the baselines are organized, one file per error identifier under tests/phpstan/baselines, and how to regenerate them with `composer phpstan:baselines`. The organization section explains why the entries carry a path and an exact count, which is easy to read as incidental detail rather than as the mechanism that makes a new occurrence of an already baselined error report as new. It also spells out the consequence that catches people out: fixing a baselined error means regenerating its baseline in the same change, because the count no longer matches, and the resulting `ignore.count` error cannot itself be ignored. The example entry is copied from the generated baseline rather than written by hand, so it cannot drift into showing a shape that is not produced. Co-Authored-By: Claude Opus 5 (1M context) --- tests/phpstan/README.md | 48 ++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index 036f4b98432e3..553789681e324 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -91,20 +91,52 @@ PHPStan errors can be ignored in the following ways: - Adding the error pattern to the `ignoreErrors` section of the `phpstan.neon.dist` configuration file. This should be used to handle conflicts with WordPress Coding Standards or similar project decisions, or to allowlist legacy code that is not worth refactoring solely to satisfy the tests. -- Adding an error to the "tech debt" baseline. This should be used for code that needs to be addressed eventually - by fixing, refactoring, or ignoring via one of the above methods - but is not worth addressing right now. +- Adding an error to a "tech debt" baseline. This should be used for code that needs to be addressed eventually - by fixing, refactoring, or ignoring via one of the above methods - but is not worth addressing right now. Baselines are a useful triage tool for handling PHPStan errors in legacy code, as they allow us to enforce stricter code quality checks on new code, while gradually chipping away at the existing issues over time. **Avoid adding PHPStan errors from new code whenever possible, and use baselines as a last resort.** - The baseline file is located at `tests/phpstan/baseline.php` and generated by running PHPStan with the `--generate-baseline` flag: +### How the baselines are organized - ```bash - npm run typecheck:php -- --generate-baseline=tests/phpstan/baseline.php +The baselines live in [`baselines/`](baselines), one file per error identifier, such as `variable.undefined.neon`. Splitting them this way keeps each kind of error visible as a single file that should shrink to nothing and then be deleted, rather than as part of one large file in which every kind is mixed together. - # or, with Composer directly: - composer run phpstan -- --generate-baseline=tests/phpstan/baseline.php - ``` +Every entry is scoped to the file the error occurs in and carries an exact occurrence count: - This will regenerate the baseline file with any new errors added to the existing ones. You can then commit the updated baseline file. +```neon +- + message: '#^Variable \$wpdb might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: ../../../src/wp-trackback.php +``` + +Both the path and the count matter. A new occurrence of an already baselined error does not match the entry, even in a file that is already listed, and is reported as a new error. That is the point of recording them this way: the baselines describe exactly what exists today, so nothing new slips in behind them. + +The consequence is that **fixing a baselined error means regenerating its baseline as part of the same change**, because the count no longer matches. A count that no longer matches is reported as an `ignore.count` error, which PHPStan does not allow to be ignored or baselined. + +### Regenerating the baselines + +The baselines are generated, and should not be edited by hand. Regenerate them with: + +```bash +# every identifier: +composer phpstan:baselines + +# a single identifier: +composer phpstan:baselines -- --identifier=variable.undefined + +# several, either comma separated or by repeating the option: +composer phpstan:baselines -- --identifier=variable.undefined,isset.variable +composer phpstan:baselines -- --identifier=isset.variable --identifier=empty.variable + +# print every error as one baseline, writing nothing: +composer phpstan:baselines -- --combined +``` + +Run `composer phpstan:baselines -- --help` for the remaining options. + +A run also deletes any baseline whose identifier no longer reports anything, and rewrites the list of them between the `# phpstan:baselines` markers in the `includes` of [`phpstan.neon.dist`](../../phpstan.neon.dist) to match. Adding a newly split out baseline, and retiring one that has reached zero, therefore need no edit of the configuration. + +PHPStan's own `--generate-baseline` is deliberately not used directly. It captures every error a run reports, with no way to restrict it to one identifier, so it cannot refresh a single baseline without sweeping every other kind of error into it. ## Performance and troubleshooting From 1c18559bf37d6dc38e3af93a651bcb128638d17c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 21:53:39 -0700 Subject: [PATCH 41/51] Build/Test Tools: Add an npm script to regenerate the baselines The baselines could only be regenerated by calling Composer directly, so anyone working in the Docker environment had no documented way to do it. Add `typecheck:php:baselines` alongside `typecheck:php`, which runs the generator in the php container the same way, and document it first in the README, matching how the analysis itself is documented there. The script ends in `--`. Composer reads anything before that as its own options rather than passing it to the script, and discards what it does not recognize without saying so, so `composer phpstan:baselines --identifier=variable.undefined` quietly regenerates every baseline instead of that one. Supplying the separator in the script means a single `--` is enough from npm, which is what the rest of the README already tells people to write, rather than the two that would otherwise be needed. The README notes the same trap for anyone invoking Composer directly, since there the `--` has to be written by hand and its absence is silent. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 1 + tests/phpstan/README.md | 30 ++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d854406d50d7e..6715f559289cf 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", + "typecheck:php:baselines": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan:baselines --", "gutenberg:copy": "node tools/gutenberg/copy.js", "gutenberg:verify": "node tools/gutenberg/utils.js", "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg" diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index 553789681e324..14108355edf12 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -118,21 +118,39 @@ The consequence is that **fixing a baselined error means regenerating its baseli The baselines are generated, and should not be edited by hand. Regenerate them with: ```bash -# every identifier: -composer phpstan:baselines +npm run typecheck:php:baselines +``` + +which will run the generator in the Docker container. + +As with the analysis itself, flags are passed by adding `--` followed by the flags themselves: +```bash # a single identifier: -composer phpstan:baselines -- --identifier=variable.undefined +npm run typecheck:php:baselines -- --identifier=variable.undefined # several, either comma separated or by repeating the option: -composer phpstan:baselines -- --identifier=variable.undefined,isset.variable -composer phpstan:baselines -- --identifier=isset.variable --identifier=empty.variable +npm run typecheck:php:baselines -- --identifier=variable.undefined,isset.variable +npm run typecheck:php:baselines -- --identifier=isset.variable --identifier=empty.variable # print every error as one baseline, writing nothing: +npm run typecheck:php:baselines -- --combined + +# the remaining options: +npm run typecheck:php:baselines -- --help +``` + +If you are not using the Docker environment, you can run the generator via Composer directly: + +```bash +composer phpstan:baselines + +composer phpstan:baselines -- --identifier=variable.undefined composer phpstan:baselines -- --combined +composer phpstan:baselines -- --help ``` -Run `composer phpstan:baselines -- --help` for the remaining options. +Note the `--` in each of those. Composer needs it in order to pass the flags on to the script rather than reading them as its own, and without it they are discarded silently, so `composer phpstan:baselines --identifier=variable.undefined` regenerates every baseline rather than that one. The npm script supplies it, which is why only one is needed there. A run also deletes any baseline whose identifier no longer reports anything, and rewrites the list of them between the `# phpstan:baselines` markers in the `includes` of [`phpstan.neon.dist`](../../phpstan.neon.dist) to match. Adding a newly split out baseline, and retiring one that has reached zero, therefore need no edit of the configuration. From 4728bf449392ef10f31bb0a0072a20c28a7c850d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 21:58:34 -0700 Subject: [PATCH 42/51] Build/Test Tools: Pass flags through typecheck:php to PHPStan `npm run typecheck:php -- --memory-limit=4G` resolved to `composer phpstan --memory-limit=4G`, with no `--` for Composer to pass the flag on by. Composer read it as one of its own, did not recognize it, and discarded it without saying so, so the analysis ran with the default memory limit. The same went for every other flag the README documents passing this way, including the file path that is supposed to narrow the run to a single file. Composer's handling is easy to confirm: $ composer phpstan --version Composer version 2.10.2 $ composer phpstan -- --version PHPStan - PHP Static Analysis Tool 2.2.5 End the script in `--`, as typecheck:php:baselines already does. A single `--` from npm is then enough, which is what the README already tells people to write, and a run with no flags passes a bare separator that PHPStan ignores. The Composer examples in the README were already correct, since they spell the separator out. Note there why it is needed, so it does not read as incidental and get dropped. env:start and env:composer are left alone. The first passes a flag belonging to Composer itself, and the second is a passthrough for arbitrary Composer commands, which a trailing separator would break. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 2 +- tests/phpstan/README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6715f559289cf..5264d752b8e4e 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "test:coverage": "npm run test:php -- --coverage-html ./coverage/html/ --coverage-php ./coverage/php/report.php --coverage-text=./coverage/text/report.txt", "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", - "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", + "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan --", "typecheck:php:baselines": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan:baselines --", "gutenberg:copy": "node tools/gutenberg/copy.js", "gutenberg:verify": "node tools/gutenberg/utils.js", diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index 14108355edf12..edf96fefdc093 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -39,6 +39,8 @@ composer run phpstan -- src/wp-includes/template.php composer run phpstan -- -vvv --debug ``` +Note the `--` in each of those. Composer needs it in order to pass the flags on to PHPStan rather than reading them as its own, and without it they are discarded silently. The npm script supplies it, which is why only one is needed there. + For available flags, see https://phpstan.org/user-guide/command-line-usage. ## The PHPStan configuration From d976b1a2d242a6224d7e00a86c0c83c2db336b30 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 22:11:08 -0700 Subject: [PATCH 43/51] Build/Test Tools: Say what to do when a baseline goes out of date Fixing a baselined error makes PHPStan report an unmatched ignore, because the baselines record an exact count per file and the entry now describes a state that no longer exists. The fix is to regenerate the baselines, not to change anything in the code, but nothing in the job says so. That is a bad message to leave to inference. The analysis is piped through cs2pr with --errors-as-warnings --graceful-warnings, so the run passes and the only trace is an annotation reading `Ignored error pattern ... was not matched in reported errors` against a line the contributor has just fixed correctly. Read plainly, it looks like the fix is what upset the tool. Detect those reports and fail the job with an explanation instead. The step writes to the job summary, where there is room to say why it happens and that the fix is expected, and adds an error annotation with the command to run. It fails rather than warns because a stale baseline is a repository state problem and not a judgment about the code: merged, it puts the same unmatched ignores in front of everyone else. Detection greps the checkstyle report, which the analysis step now also writes to a file, for the two phrasings PHPStan uses. Verified against a report from a tree where the baselined errors had been fixed, which is the case that matters, and against a report carrying ordinary errors only, which must not trigger it. Also fix the path filter that decides whether this workflow runs at all. It still named tests/phpstan/baseline.php, which no longer exists, and named nothing under tests/phpstan/baselines, so a pull request that only regenerated the baselines would not have run the analysis that checks them. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/phpstan-static-analysis.yml | 2 +- .../reusable-phpstan-static-analysis-v1.yml | 54 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpstan-static-analysis.yml b/.github/workflows/phpstan-static-analysis.yml index 62061a83a2688..7d7043ac0c1ec 100644 --- a/.github/workflows/phpstan-static-analysis.yml +++ b/.github/workflows/phpstan-static-analysis.yml @@ -18,7 +18,7 @@ on: # These files configure PHPStan. Changes could affect the outcome. - 'phpstan.neon.dist' - 'tests/phpstan/base.neon' - - 'tests/phpstan/baseline.php' + - 'tests/phpstan/baselines/**' # Confirm any changes to relevant workflow files. - '.github/workflows/phpstan-static-analysis.yml' - '.github/workflows/reusable-phpstan-static-analysis-v1.yml' diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml index a69b3b46fdea4..3643fff33f059 100644 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -32,6 +32,7 @@ jobs: # - Builds WordPress. # - Configures caching for PHPStan static analysis scans. # - Runs PHPStan static analysis (with Pull Request annotations). + # - Checks whether the baselines need regenerating. # - Saves the PHPStan result cache. # - Ensures version-controlled files are not modified or deleted. phpstan: @@ -93,7 +94,58 @@ jobs: - name: Run PHP static analysis tests id: phpstan - run: composer run phpstan -- -vvv --error-format=checkstyle | cs2pr --errors-as-warnings --graceful-warnings + run: composer run phpstan -- -vvv --error-format=checkstyle | tee "${RUNNER_TEMP}/phpstan-report.xml" | cs2pr --errors-as-warnings --graceful-warnings + + # A baselined error that no longer occurs, or occurs a different number of times, is + # reported as an unmatched ignore. That is not something to fix in the code: the usual + # cause is that the error *was* fixed, leaving the baseline describing a state that no + # longer exists. PHPStan does not allow those reports to be ignored or baselined. + # + # The analysis above is reported as warnings, so this would otherwise surface as a + # passing run carrying an annotation that reads like a complaint about a fix. Call it + # out on its own, and say what to do about it. + - name: Check whether the baselines need regenerating + if: ${{ !cancelled() }} + run: | + if [ ! -f "${RUNNER_TEMP}/phpstan-report.xml" ]; then + exit 0 + fi + + if ! grep -qE 'was not matched in reported errors|is expected to occur' "${RUNNER_TEMP}/phpstan-report.xml"; then + exit 0 + fi + + { + echo '## PHPStan baselines are out of date' + echo + echo 'A baselined error no longer occurs, or occurs a different number of times, so' + echo 'PHPStan reported an `ignore.unmatched` or `ignore.count` error.' + echo + echo '**If you fixed the error, this is expected.** Each baseline entry records an exact' + echo 'count for a specific file, so that a new occurrence of an already baselined error is' + echo 'reported rather than absorbed. That same exactness means fixing one leaves the' + echo 'baseline describing a state that no longer exists. There is nothing to fix in the' + echo 'code; the baselines just need to catch up.' + echo + echo 'Regenerate them and commit the result:' + echo + echo '```bash' + echo 'npm run typecheck:php:baselines' + echo '```' + echo + echo 'or, outside the Docker environment:' + echo + echo '```bash' + echo 'composer phpstan:baselines' + echo '```' + echo + echo 'That rewrites the files under `tests/phpstan/baselines`, deletes any whose errors are' + echo 'now all fixed, and updates the list of them in `phpstan.neon.dist`. See' + echo '`tests/phpstan/README.md` for details.' + } >> "${GITHUB_STEP_SUMMARY}" + + echo "::error title=PHPStan baselines are out of date::A baselined error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` and commit the updated baselines. See tests/phpstan/README.md." + exit 1 - name: "Save result cache" uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 From 25aee3fba7e9fe9533bf9c27effe8ae3259e9d78 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 22:18:12 -0700 Subject: [PATCH 44/51] Build/Test Tools: Detect stale baselines by identifier, and link the docs Detection matched two phrasings of the message PHPStan prints for an unmatched ignore. The message is prose, reworded whenever the wording is improved, and nothing fails when it stops matching: the check simply goes quiet and the confusing annotation it exists to explain comes back. The identifier is the stable name for the report, and the checkstyle format carries it in the `source` attribute, so match on that instead. Matching the `ignore.` prefix rather than the two identifiers seen so far also covers any later addition, since every report in that group means the same thing, that the ignore configuration no longer describes the code. Say what to do when the report names an `@phpstan-ignore` annotation rather than a baseline entry, which is the one case where regenerating does not help and the annotation has to be removed by hand. Link the README rather than naming its path. The job summary is Markdown, so it can carry a real link, and the annotation can carry the URL. Both are built from the commit being tested, so the documentation shown is the documentation that came with the change. Co-Authored-By: Claude Opus 5 (1M context) --- .../reusable-phpstan-static-analysis-v1.yml | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml index 3643fff33f059..cbe38559d32dc 100644 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -96,30 +96,35 @@ jobs: id: phpstan run: composer run phpstan -- -vvv --error-format=checkstyle | tee "${RUNNER_TEMP}/phpstan-report.xml" | cs2pr --errors-as-warnings --graceful-warnings - # A baselined error that no longer occurs, or occurs a different number of times, is - # reported as an unmatched ignore. That is not something to fix in the code: the usual - # cause is that the error *was* fixed, leaving the baseline describing a state that no - # longer exists. PHPStan does not allow those reports to be ignored or baselined. + # An ignored error that no longer occurs, or occurs a different number of times, is + # reported under an `ignore.*` identifier. That is not something to fix in the code: the + # usual cause is that the error *was* fixed, leaving a baseline describing a state that + # no longer exists. PHPStan does not allow those reports to be ignored or baselined. # # The analysis above is reported as warnings, so this would otherwise surface as a # passing run carrying an annotation that reads like a complaint about a fix. Call it # out on its own, and say what to do about it. + # + # Detection is on the identifier rather than the message, which is prose and may be + # reworded in any release. The checkstyle format carries it in the `source` attribute. - name: Check whether the baselines need regenerating if: ${{ !cancelled() }} + env: + README_URL: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/tests/phpstan/README.md run: | if [ ! -f "${RUNNER_TEMP}/phpstan-report.xml" ]; then exit 0 fi - if ! grep -qE 'was not matched in reported errors|is expected to occur' "${RUNNER_TEMP}/phpstan-report.xml"; then + if ! grep -q 'source="ignore\.' "${RUNNER_TEMP}/phpstan-report.xml"; then exit 0 fi { echo '## PHPStan baselines are out of date' echo - echo 'A baselined error no longer occurs, or occurs a different number of times, so' - echo 'PHPStan reported an `ignore.unmatched` or `ignore.count` error.' + echo 'An ignored error no longer occurs, or occurs a different number of times, so' + echo 'PHPStan reported it under an `ignore.unmatched` or `ignore.count` identifier.' echo echo '**If you fixed the error, this is expected.** Each baseline entry records an exact' echo 'count for a specific file, so that a new occurrence of an already baselined error is' @@ -140,11 +145,15 @@ jobs: echo '```' echo echo 'That rewrites the files under `tests/phpstan/baselines`, deletes any whose errors are' - echo 'now all fixed, and updates the list of them in `phpstan.neon.dist`. See' - echo '`tests/phpstan/README.md` for details.' + echo 'now all fixed, and updates the list of them in `phpstan.neon.dist`.' + echo + echo 'Where the report names an `@phpstan-ignore` annotation in the code rather than a' + echo 'baseline entry, remove that annotation instead; regenerating will not clear it.' + echo + echo "See [tests/phpstan/README.md](${README_URL}) for details." } >> "${GITHUB_STEP_SUMMARY}" - echo "::error title=PHPStan baselines are out of date::A baselined error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` and commit the updated baselines. See tests/phpstan/README.md." + echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` and commit the updated baselines. See ${README_URL}" exit 1 - name: "Save result cache" From 00d4094b9ff03bad5cbf7600cdb073d785bbc64d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 22:23:15 -0700 Subject: [PATCH 45/51] Build/Test Tools: Comment the two early exits in the baseline check Both guards in the step read as unexplained. The first tests for a file whose absence is not obviously possible, and the second matches a string prefix that names no identifier a reader can look up, so neither says why the step would stop there. Give each one a comment. The first notes that the step runs even when the analysis before it failed, so the report may never have been written, and that failing on its absence would only obscure the real failure. The second names `ignore.unmatched` and `ignore.count` as what the prefix catches, and says why the prefix is matched rather than the two names. Co-Authored-By: Claude Opus 5 (1M context) --- .../workflows/reusable-phpstan-static-analysis-v1.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml index cbe38559d32dc..968a08b2a6ff3 100644 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -112,10 +112,19 @@ jobs: env: README_URL: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/tests/phpstan/README.md run: | + # This step runs even when the analysis above it failed, in which case the report may + # never have been written. There is nothing to check then, and failing here would + # only obscure whatever actually went wrong. if [ ! -f "${RUNNER_TEMP}/phpstan-report.xml" ]; then exit 0 fi + # Leave the run alone unless PHPStan reported an ignore error, because everything + # below concerns an ignore configuration that no longer describes the code, and + # nothing else. Those errors are `ignore.unmatched`, where a pattern matched nothing + # at all, and `ignore.count`, where it matched a different number of times than the + # entry records. The `ignore.` prefix is matched rather than those two names so that + # any later addition to the group is caught as well. if ! grep -q 'source="ignore\.' "${RUNNER_TEMP}/phpstan-report.xml"; then exit 0 fi From c0cb5f61d46a45cf32be2f2282e61451489908cf Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 22:33:16 -0700 Subject: [PATCH 46/51] Build/Test Tools: Fail the run when PHPStan does not finish An analysis that crashed passed as a green run. The report is piped through cs2pr, which exits successfully so that reported errors annotate a pull request without failing it, and a pipeline reports only the status of its last command. The status of the analysis itself was therefore discarded, and it was the only sign that anything had gone wrong. An out of memory kill or a parse error in the configuration looked exactly like a clean run. Recover that status from PIPESTATUS and act on it. PHPStan exits 1 when it has errors to report, which is the expected case here and is what the annotations are for, so only a status above that is treated as a failure to finish. Setting pipefail would not work, since it would fail the run for reported errors too, which is the case cs2pr is configured to tolerate. Verified against stubbed exit statuses: 0 and 1 pass through, while 2, 137 and 255 emit an annotation and fail the step. Also say in phpstan.neon.dist what its ignoreErrors are for, now that the baselines it includes hold the same kind of entry with the opposite intent. A baseline records work still to be done and is meant to reach zero and be deleted; an entry in the configuration is a decision that the code is right as written. Without that written down, the two are indistinguishable, and the easiest way to quiet a report is to add it to whichever list is closer to hand. Co-Authored-By: Claude Opus 5 (1M context) --- .../reusable-phpstan-static-analysis-v1.yml | 22 ++++++++++++++++--- phpstan.neon.dist | 11 ++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml index 968a08b2a6ff3..9d8516840d29c 100644 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -94,7 +94,23 @@ jobs: - name: Run PHP static analysis tests id: phpstan - run: composer run phpstan -- -vvv --error-format=checkstyle | tee "${RUNNER_TEMP}/phpstan-report.xml" | cs2pr --errors-as-warnings --graceful-warnings + run: | + # The report is written to a file as well as piped to cs2pr, so that the step below + # can look at it. + # + # cs2pr exits successfully so that reported errors annotate the pull request without + # failing the run. A pipeline reports only the status of its last command, so that + # also discards the status of the analysis itself. Recover it from PIPESTATUS. + composer run phpstan -- -vvv --error-format=checkstyle | tee "${RUNNER_TEMP}/phpstan-report.xml" | cs2pr --errors-as-warnings --graceful-warnings + status="${PIPESTATUS[0]}" + + # PHPStan exits 1 when it has errors to report, which is the expected case here and + # is what the annotations are for. Anything higher means it did not finish at all, + # which would otherwise pass silently, since the discarded status was the only sign. + if [ "${status}" -gt 1 ]; then + echo "::error title=PHPStan did not complete::The analysis exited with status ${status}, so the code was not fully checked. This is a failure of the run itself rather than a problem found in the code." + exit "${status}" + fi # An ignored error that no longer occurs, or occurs a different number of times, is # reported under an `ignore.*` identifier. That is not something to fix in the code: the @@ -113,8 +129,8 @@ jobs: README_URL: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/tests/phpstan/README.md run: | # This step runs even when the analysis above it failed, in which case the report may - # never have been written. There is nothing to check then, and failing here would - # only obscure whatever actually went wrong. + # never have been written. That failure is reported there, so there is nothing to add + # here beyond staying quiet about a file that was never going to exist. if [ ! -f "${RUNNER_TEMP}/phpstan-report.xml" ]; then exit 0 fi diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3f56518228eed..778b24b78c465 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -32,6 +32,17 @@ parameters: level: 1 reportUnmatchedIgnoredErrors: true + # The following ignored errors are not intended to be fixed, as distinct from the baselines + # included above. + # + # A baseline records work still to be done. Every entry in one is in scope to be fixed, and + # each file is meant to reach zero and then be deleted. An entry here is the opposite: a + # decision that the code is right as written and the report is not actionable, whether + # because PHPStan cannot see what makes the code safe, or because satisfying it would mean + # changing code that has no other reason to change. + # + # So prefer fixing an error, and baseline it when it cannot be fixed yet. Add it here only + # when it should never be fixed, and say why. ignoreErrors: # Level 0: - # Inner functions aren't supported by PHPStan. From d5430c5a502ea3c0e5dfc0bffd5510ad10ea678c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 22:56:07 -0700 Subject: [PATCH 47/51] Build/Test Tools: Write the baseline summary with a heredoc actionlint failed the workflow lint. The job summary was built from a run of echo calls, and the ones carrying Markdown code spans put backticks inside single quotes, which shellcheck reports as SC2016: a command substitution that will not expand. The backticks are meant literally, so the report is a false positive, but it is one the linter is right to make about that shape of code. Write the summary as a quoted heredoc instead. Nothing in it is expanded, so the backticks are no longer the shell's concern, and the Markdown reads as Markdown rather than as thirty quoted fragments. The one line needing a variable is appended after it, and has no backticks to quote. Verified by extracting both `run` blocks from the workflow and running shellcheck over them at the severity actionlint reports at, which is clean, and by running the step against a real checkstyle report to confirm the summary still renders with its fences and code spans intact. Co-Authored-By: Claude Opus 5 (1M context) --- .../reusable-phpstan-static-analysis-v1.yml | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml index 9d8516840d29c..4de6e0e8aa40f 100644 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -145,38 +145,42 @@ jobs: exit 0 fi - { - echo '## PHPStan baselines are out of date' - echo - echo 'An ignored error no longer occurs, or occurs a different number of times, so' - echo 'PHPStan reported it under an `ignore.unmatched` or `ignore.count` identifier.' - echo - echo '**If you fixed the error, this is expected.** Each baseline entry records an exact' - echo 'count for a specific file, so that a new occurrence of an already baselined error is' - echo 'reported rather than absorbed. That same exactness means fixing one leaves the' - echo 'baseline describing a state that no longer exists. There is nothing to fix in the' - echo 'code; the baselines just need to catch up.' - echo - echo 'Regenerate them and commit the result:' - echo - echo '```bash' - echo 'npm run typecheck:php:baselines' - echo '```' - echo - echo 'or, outside the Docker environment:' - echo - echo '```bash' - echo 'composer phpstan:baselines' - echo '```' - echo - echo 'That rewrites the files under `tests/phpstan/baselines`, deletes any whose errors are' - echo 'now all fixed, and updates the list of them in `phpstan.neon.dist`.' - echo - echo 'Where the report names an `@phpstan-ignore` annotation in the code rather than a' - echo 'baseline entry, remove that annotation instead; regenerating will not clear it.' - echo - echo "See [tests/phpstan/README.md](${README_URL}) for details." - } >> "${GITHUB_STEP_SUMMARY}" + # The summary is Markdown, and its code spans and fences are written literally, so the + # heredoc is quoted to keep the backticks out of the shell's hands. That leaves the + # one line needing a variable to be appended separately. + cat >> "${GITHUB_STEP_SUMMARY}" <<'SUMMARY' + ## PHPStan baselines are out of date + + An ignored error no longer occurs, or occurs a different number of times, so PHPStan + reported it under an `ignore.unmatched` or `ignore.count` identifier. + + **If you fixed the error, this is expected.** Each baseline entry records an exact + count for a specific file, so that a new occurrence of an already baselined error is + reported rather than absorbed. That same exactness means fixing one leaves the + baseline describing a state that no longer exists. There is nothing to fix in the + code; the baselines just need to catch up. + + Regenerate them and commit the result: + + ```bash + npm run typecheck:php:baselines + ``` + + or, outside the Docker environment: + + ```bash + composer phpstan:baselines + ``` + + That rewrites the files under `tests/phpstan/baselines`, deletes any whose errors are + now all fixed, and updates the list of them in `phpstan.neon.dist`. + + Where the report names an `@phpstan-ignore` annotation in the code rather than a + baseline entry, remove that annotation instead; regenerating will not clear it. + + SUMMARY + + echo "See [tests/phpstan/README.md](${README_URL}) for details." >> "${GITHUB_STEP_SUMMARY}" echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` and commit the updated baselines. See ${README_URL}" exit 1 From 8ebe86221f984e1b542635ba2a675bdee95650c5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 23:25:54 -0700 Subject: [PATCH 48/51] Try fixing one PHPStan error to see the impact on the job --- src/wp-admin/admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php index 82ab6b93ac99e..39ea85470a265 100644 --- a/src/wp-admin/admin.php +++ b/src/wp-admin/admin.php @@ -210,7 +210,7 @@ $hook_suffix = $page_hook; } elseif ( isset( $plugin_page ) ) { $hook_suffix = $plugin_page; -} elseif ( isset( $pagenow ) ) { +} else { $hook_suffix = $pagenow; } From cb83dc33aa331876fb4aa31ad3f77cacefbbc899 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 23:34:59 -0700 Subject: [PATCH 49/51] Add composer command to the error message --- .github/workflows/reusable-phpstan-static-analysis-v1.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml index 4de6e0e8aa40f..9a12728c4dad7 100644 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -182,7 +182,7 @@ jobs: echo "See [tests/phpstan/README.md](${README_URL}) for details." >> "${GITHUB_STEP_SUMMARY}" - echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` and commit the updated baselines. See ${README_URL}" + echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` or \`composer phpstan:baselines\` and commit the updated baselines. See ${README_URL}" exit 1 - name: "Save result cache" From 7b7007b20637886c8d8e63dcd12d9289d6088e0c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 23:48:20 -0700 Subject: [PATCH 50/51] Build/Test Tools: Let the baseline summary wrap, and link the baselines The summary was written wrapped at the width of the workflow file, which is not the width it is read at. A newline renders as a line break there rather than as a space, so the paragraphs kept those breaks and read as ragged columns down the left of a much wider panel. Put each paragraph on one line and let the rendered summary wrap to its own width. Link `tests/phpstan/baselines` as well, since the summary is telling the reader about a directory they may never have opened, and it was already linking the README beside it. Both links are written in reference style. The URLs are the only part of the summary that needs a variable, and the heredoc holding it is quoted so that its code spans and fences stay literal, so defining the URLs after it keeps all of the prose inside the one block rather than splitting it around the two lines that interpolate. Co-Authored-By: Claude Opus 5 (1M context) --- .../reusable-phpstan-static-analysis-v1.yml | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml index 9a12728c4dad7..26a14ba8d890f 100644 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -126,6 +126,7 @@ jobs: - name: Check whether the baselines need regenerating if: ${{ !cancelled() }} env: + BASELINES_URL: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.sha }}/tests/phpstan/baselines README_URL: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/tests/phpstan/README.md run: | # This step runs even when the analysis above it failed, in which case the report may @@ -146,19 +147,18 @@ jobs: fi # The summary is Markdown, and its code spans and fences are written literally, so the - # heredoc is quoted to keep the backticks out of the shell's hands. That leaves the - # one line needing a variable to be appended separately. + # heredoc is quoted to keep the backticks out of the shell's hands. The links are + # written in reference style for the same reason: the URLs are the only part needing + # a variable, so defining them afterwards keeps the whole of the prose in here. + # + # A newline renders as a line break rather than a space, so each paragraph is one + # line however long that makes it, and the rendered summary wraps to its own width. cat >> "${GITHUB_STEP_SUMMARY}" <<'SUMMARY' ## PHPStan baselines are out of date - An ignored error no longer occurs, or occurs a different number of times, so PHPStan - reported it under an `ignore.unmatched` or `ignore.count` identifier. + An ignored error no longer occurs, or occurs a different number of times, so PHPStan reported it under an `ignore.unmatched` or `ignore.count` identifier. - **If you fixed the error, this is expected.** Each baseline entry records an exact - count for a specific file, so that a new occurrence of an already baselined error is - reported rather than absorbed. That same exactness means fixing one leaves the - baseline describing a state that no longer exists. There is nothing to fix in the - code; the baselines just need to catch up. + **If you fixed the error, this is expected.** Each baseline entry records an exact count for a specific file, so that a new occurrence of an already baselined error is reported rather than absorbed. That same exactness means fixing one leaves the baseline describing a state that no longer exists. There is nothing to fix in the code; the baselines just need to catch up. Regenerate them and commit the result: @@ -172,15 +172,18 @@ jobs: composer phpstan:baselines ``` - That rewrites the files under `tests/phpstan/baselines`, deletes any whose errors are - now all fixed, and updates the list of them in `phpstan.neon.dist`. + That rewrites the files under [`tests/phpstan/baselines`][baselines], deletes any whose errors are now all fixed, and updates the list of them in `phpstan.neon.dist`. + + Where the report names an `@phpstan-ignore` annotation in the code rather than a baseline entry, remove that annotation instead; regenerating will not clear it. - Where the report names an `@phpstan-ignore` annotation in the code rather than a - baseline entry, remove that annotation instead; regenerating will not clear it. + See [`tests/phpstan/README.md`][readme] for details. SUMMARY - echo "See [tests/phpstan/README.md](${README_URL}) for details." >> "${GITHUB_STEP_SUMMARY}" + { + echo "[baselines]: ${BASELINES_URL}" + echo "[readme]: ${README_URL}" + } >> "${GITHUB_STEP_SUMMARY}" echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` or \`composer phpstan:baselines\` and commit the updated baselines. See ${README_URL}" exit 1 From 41ec803b9f6ac65a962d420faca1dd4e77b08bd1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 4 Aug 2026 23:52:03 -0700 Subject: [PATCH 51/51] Revert "Try fixing one PHPStan error to see the impact on the job" This reverts commit 8ebe86221f984e1b542635ba2a675bdee95650c5. --- src/wp-admin/admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php index 39ea85470a265..82ab6b93ac99e 100644 --- a/src/wp-admin/admin.php +++ b/src/wp-admin/admin.php @@ -210,7 +210,7 @@ $hook_suffix = $page_hook; } elseif ( isset( $plugin_page ) ) { $hook_suffix = $plugin_page; -} else { +} elseif ( isset( $pagenow ) ) { $hook_suffix = $pagenow; }