From ca178c9f24e227fcb4e00815765323657164fcbd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 09:49:55 -0700 Subject: [PATCH 1/8] Revert "Move is_tag_closer conditions into else which fixes deadCode.unreachable and if.alwaysTrue errors" This reverts commit 0f13b8511d269dba513dbda6dd9ed012eb093051. --- .../html-api/class-wp-html-processor.php | 26 +++++++++++-------- .../baselines/booleanAnd.leftAlwaysTrue.neon | 5 ++++ .../baselines/deadCode.unreachable.neon | 2 +- tests/phpstan/baselines/if.alwaysTrue.neon | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 9eceb18729c1f..5fb724c49043d 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -4963,18 +4963,20 @@ private function step_in_foreign_content(): bool { $this->state->stack_of_open_elements->pop(); } return true; - } else { - /* - * > An end tag whose name is "script", if the current node is an SVG script element. - */ - if ( 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) { - $this->state->stack_of_open_elements->pop(); - return true; - } + } - /* - * > Any other end tag - */ + /* + * > An end tag whose name is "script", if the current node is an SVG script element. + */ + if ( $this->is_tag_closer() && 'SCRIPT' === $this->state->current_token->node_name && 'svg' === $this->state->current_token->namespace ) { + $this->state->stack_of_open_elements->pop(); + return true; + } + + /* + * > Any other end tag + */ + if ( $this->is_tag_closer() ) { $node = $this->state->stack_of_open_elements->current_node(); if ( $tag_name !== $node->node_name ) { // @todo Indicate a parse error once it's possible. @@ -5074,6 +5076,8 @@ private function step_in_foreign_content(): bool { $this->bail( "Unaware of the requested parsing mode: '{$this->state->insertion_mode}'." ); } } + + $this->bail( 'Should not have been able to reach end of IN FOREIGN CONTENT processing. Check HTML API code.' ); } /* diff --git a/tests/phpstan/baselines/booleanAnd.leftAlwaysTrue.neon b/tests/phpstan/baselines/booleanAnd.leftAlwaysTrue.neon index 2ea5a45c3cfc2..69e4b48b16039 100644 --- a/tests/phpstan/baselines/booleanAnd.leftAlwaysTrue.neon +++ b/tests/phpstan/baselines/booleanAnd.leftAlwaysTrue.neon @@ -33,3 +33,8 @@ parameters: identifier: booleanAnd.leftAlwaysTrue count: 1 path: ../../../src/wp-includes/canonical.php + - + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue + count: 1 + path: ../../../src/wp-includes/html-api/class-wp-html-processor.php diff --git a/tests/phpstan/baselines/deadCode.unreachable.neon b/tests/phpstan/baselines/deadCode.unreachable.neon index 22f5a17da9286..bf61e95da6829 100644 --- a/tests/phpstan/baselines/deadCode.unreachable.neon +++ b/tests/phpstan/baselines/deadCode.unreachable.neon @@ -56,7 +56,7 @@ parameters: - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable - count: 12 + count: 13 path: ../../../src/wp-includes/html-api/class-wp-html-processor.php - message: '#^Unreachable statement \- code above always terminates\.$#' diff --git a/tests/phpstan/baselines/if.alwaysTrue.neon b/tests/phpstan/baselines/if.alwaysTrue.neon index ffd7d9081b25f..6888b9780b5f9 100644 --- a/tests/phpstan/baselines/if.alwaysTrue.neon +++ b/tests/phpstan/baselines/if.alwaysTrue.neon @@ -46,5 +46,5 @@ parameters: - message: '#^If condition is always true\.$#' identifier: if.alwaysTrue - count: 1 + count: 2 path: ../../../src/wp-includes/html-api/class-wp-html-processor.php From 47c2e8e98c19b443333d377fc307265482b53aff Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 09:50:04 -0700 Subject: [PATCH 2/8] Revert "Remove else statement which allows removal of break for phpcs" This reverts commit c9ecea45e9f1cc88a61077873dbf0ebf957a0a3b. --- .../html-api/class-wp-html-processor.php | 35 ++++++++++--------- .../baselines/deadCode.unreachable.neon | 2 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 5fb724c49043d..ed7ef49b0253c 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -2773,27 +2773,28 @@ private function step_in_body(): bool { $this->state->stack_of_open_elements->remove_node( $node ); return true; - } + } else { + /* + * > If the stack of open elements does not have a form element in scope, + * > then this is a parse error; return and ignore the token. + * + * Note that unlike in the clause above, this is checking for any FORM in scope. + */ + if ( ! $this->state->stack_of_open_elements->has_element_in_scope( 'FORM' ) ) { + // Parse error: ignore the token. + return $this->step(); + } - /* - * > If the stack of open elements does not have a form element in scope, - * > then this is a parse error; return and ignore the token. - * - * Note that unlike in the clause above, this is checking for any FORM in scope. - */ - if ( ! $this->state->stack_of_open_elements->has_element_in_scope( 'FORM' ) ) { - // Parse error: ignore the token. - return $this->step(); - } + $this->generate_implied_end_tags(); - $this->generate_implied_end_tags(); + if ( ! $this->state->stack_of_open_elements->current_node_is( 'FORM' ) ) { + // @todo Indicate a parse error once it's possible. This error does not impact the logic here. + } - if ( ! $this->state->stack_of_open_elements->current_node_is( 'FORM' ) ) { - // @todo Indicate a parse error once it's possible. This error does not impact the logic here. + $this->state->stack_of_open_elements->pop_until( 'FORM' ); + return true; } - - $this->state->stack_of_open_elements->pop_until( 'FORM' ); - return true; + break; /* * > An end tag whose tag name is "p" diff --git a/tests/phpstan/baselines/deadCode.unreachable.neon b/tests/phpstan/baselines/deadCode.unreachable.neon index bf61e95da6829..4be6c0c363686 100644 --- a/tests/phpstan/baselines/deadCode.unreachable.neon +++ b/tests/phpstan/baselines/deadCode.unreachable.neon @@ -56,7 +56,7 @@ parameters: - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable - count: 13 + count: 14 path: ../../../src/wp-includes/html-api/class-wp-html-processor.php - message: '#^Unreachable statement \- code above always terminates\.$#' From 974f0a83f13aba484e0e1c4e046917cacaf00d60 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 09:53:08 -0700 Subject: [PATCH 3/8] Move error ignoring from baseline to phpstan.neon.dist since intentional --- phpstan.neon.dist | 15 +++++++++++++++ .../baselines/booleanAnd.leftAlwaysTrue.neon | 5 ----- tests/phpstan/baselines/deadCode.unreachable.neon | 5 ----- tests/phpstan/baselines/if.alwaysTrue.neon | 5 ----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d6ba7f5511b3b..45e04ac1359d6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -163,3 +163,18 @@ parameters: identifier: greaterOrEqual.alwaysFalse count: 1 path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue + count: 1 + path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable + count: 14 + path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue + count: 2 + path: src/wp-includes/html-api/class-wp-html-processor.php diff --git a/tests/phpstan/baselines/booleanAnd.leftAlwaysTrue.neon b/tests/phpstan/baselines/booleanAnd.leftAlwaysTrue.neon index 69e4b48b16039..2ea5a45c3cfc2 100644 --- a/tests/phpstan/baselines/booleanAnd.leftAlwaysTrue.neon +++ b/tests/phpstan/baselines/booleanAnd.leftAlwaysTrue.neon @@ -33,8 +33,3 @@ parameters: identifier: booleanAnd.leftAlwaysTrue count: 1 path: ../../../src/wp-includes/canonical.php - - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue - count: 1 - path: ../../../src/wp-includes/html-api/class-wp-html-processor.php diff --git a/tests/phpstan/baselines/deadCode.unreachable.neon b/tests/phpstan/baselines/deadCode.unreachable.neon index 4be6c0c363686..1818765f34ca9 100644 --- a/tests/phpstan/baselines/deadCode.unreachable.neon +++ b/tests/phpstan/baselines/deadCode.unreachable.neon @@ -53,11 +53,6 @@ parameters: identifier: deadCode.unreachable count: 1 path: ../../../src/wp-includes/class-wp-block.php - - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable - count: 14 - path: ../../../src/wp-includes/html-api/class-wp-html-processor.php - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable diff --git a/tests/phpstan/baselines/if.alwaysTrue.neon b/tests/phpstan/baselines/if.alwaysTrue.neon index 6888b9780b5f9..a7145cf63fea5 100644 --- a/tests/phpstan/baselines/if.alwaysTrue.neon +++ b/tests/phpstan/baselines/if.alwaysTrue.neon @@ -43,8 +43,3 @@ parameters: identifier: if.alwaysTrue count: 1 path: ../../../src/wp-content/themes/twentytwenty/template-parts/modal-menu.php - - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue - count: 2 - path: ../../../src/wp-includes/html-api/class-wp-html-processor.php From 007e5118f21700796adbc5a65ca7b74a8751b200 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 10:01:32 -0700 Subject: [PATCH 4/8] Fix variable.undefined errors --- src/wp-includes/html-api/class-wp-html-processor.php | 1 + tests/phpstan/baselines/variable.undefined.neon | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index ed7ef49b0253c..545d5cc29e983 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -3372,6 +3372,7 @@ private function step_in_body(): bool { * @return bool Whether an element was found. */ private function in_body_any_other_end_tag(): bool { + $node = null; $token_name = $this->get_token_name(); /* diff --git a/tests/phpstan/baselines/variable.undefined.neon b/tests/phpstan/baselines/variable.undefined.neon index 1a7f497b923aa..f82fe31a16504 100644 --- a/tests/phpstan/baselines/variable.undefined.neon +++ b/tests/phpstan/baselines/variable.undefined.neon @@ -798,11 +798,6 @@ parameters: identifier: variable.undefined count: 1 path: ../../../src/wp-includes/general-template.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 \$current_blog might not be defined\.$#' identifier: variable.undefined From 9bf9caf6786f3510dc59c61cddc35a00ecaddcb5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 10:04:53 -0700 Subject: [PATCH 5/8] Move catch.neverThrown error from baseline to HTML API ignores --- phpstan.neon.dist | 6 ++++- .../phpstan/baselines/catch.neverThrown.neon | 25 ------------------- 2 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 tests/phpstan/baselines/catch.neverThrown.neon diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 45e04ac1359d6..5bd77b131f2a5 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -33,7 +33,6 @@ includes: - tests/phpstan/baselines/booleanNot.alwaysTrue.neon - tests/phpstan/baselines/booleanOr.alwaysFalse.neon - tests/phpstan/baselines/booleanOr.alwaysTrue.neon - - tests/phpstan/baselines/catch.neverThrown.neon - tests/phpstan/baselines/class.notFound.neon - tests/phpstan/baselines/deadCode.unreachable.neon - tests/phpstan/baselines/empty.offset.neon @@ -178,3 +177,8 @@ parameters: identifier: if.alwaysTrue count: 2 path: src/wp-includes/html-api/class-wp-html-processor.php + - + message: '#^Dead catch \- Exception is never thrown in the try block\.$#' + identifier: catch.neverThrown + count: 1 + path: src/wp-includes/html-api/class-wp-html-processor.php diff --git a/tests/phpstan/baselines/catch.neverThrown.neon b/tests/phpstan/baselines/catch.neverThrown.neon deleted file mode 100644 index 30c8a4a7cfe4b..0000000000000 --- a/tests/phpstan/baselines/catch.neverThrown.neon +++ /dev/null @@ -1,25 +0,0 @@ -# PHPStan baseline for the `catch.neverThrown` errors in WordPress core. -# -# https://phpstan.org/error-identifiers/catch.neverThrown -# -# 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=catch.neverThrown -# -# which reruns the analysis with this file suppressed so the errors surface again. - -parameters: - ignoreErrors: - - - message: '#^Dead catch \- Exception is never thrown in the try block\.$#' - identifier: catch.neverThrown - count: 1 - path: ../../../src/wp-includes/html-api/class-wp-html-processor.php From af8367384d1b0ebc52777f9bad8e2e10bbe78127 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 10:07:42 -0700 Subject: [PATCH 6/8] Fix argument.type error in bookmark_token method via string cast --- src/wp-includes/html-api/class-wp-html-processor.php | 2 +- tests/phpstan/baselines/argument.type.neon | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 545d5cc29e983..7f9759c10cd30 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -5098,7 +5098,7 @@ private function step_in_foreign_content(): bool { * @return string Name of created bookmark. */ private function bookmark_token() { - if ( ! parent::set_bookmark( ++$this->bookmark_counter ) ) { + if ( ! parent::set_bookmark( (string) ++$this->bookmark_counter ) ) { $this->last_error = self::ERROR_EXCEEDED_MAX_BOOKMARKS; throw new Exception( 'could not allocate bookmark' ); } diff --git a/tests/phpstan/baselines/argument.type.neon b/tests/phpstan/baselines/argument.type.neon index 193d2aa64f6f3..d9a9bf5456ac9 100644 --- a/tests/phpstan/baselines/argument.type.neon +++ b/tests/phpstan/baselines/argument.type.neon @@ -988,11 +988,6 @@ parameters: identifier: argument.type count: 1 path: ../../../src/wp-includes/general-template.php - - - message: '#^Parameter \#1 \$name of method WP_HTML_Tag_Processor\:\:set_bookmark\(\) expects string, int given\.$#' - identifier: argument.type - count: 1 - path: ../../../src/wp-includes/html-api/class-wp-html-processor.php - message: '#^Parameter \#1 \$user_id of function get_userdata expects int, string given\.$#' identifier: argument.type From 3befe890d5541793ba224273ec08e0c1d30cffae Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 10:18:39 -0700 Subject: [PATCH 7/8] Move false-positive identical.alwaysFalse error from baseline to HTML API ignores section Analysis from Claude Opus 5.5: > It's a false positive, not a bug. The code is correct, but PHPStan can't see that `bookmark_token()` sets `$this->last_error` before it throws. > > **Why PHPStan concludes the comparison is always false:** > > 1. `step()` opens with `if ( null !== $this->last_error ) { return false; }`. From then on, PHPStan treats `$this->last_error` as `null`. > 2. PHPStan doesn't follow side effects into a method it calls. It doesn't see that `bookmark_token()` sets `$this->last_error = self::ERROR_EXCEEDED_MAX_BOOKMARKS` just before `throw new Exception()` (line 5102). > 3. Inside the `catch`, PHPStan uses the state from the point of the throwing call, before `bookmark_token()` has done anything. So `$this->last_error` is still `null` there, and comparing it to `'exceeded-max-bookmarks'` "will always evaluate to false." > > In reality the comparison is always **true**, since that `throw` is the only way `bookmark_token()` throws an exception. > > The near-identical check at line 1179 isn't flagged because it sits at the end of a large `try` block. The many method calls inside it clear the `null` narrowing. > > **What I tested:** > - I added `@phpstan-impure` to `bookmark_token()` to see if that would make PHPStan drop the narrowing. It didn't change anything, so I reverted it. The processor file is back to its committed state, and your `identical.alwaysFalse.neon` change is untouched. > - In a minimal reproduction in the scratchpad, the error only appears when the early `null` check is present. Reading the value through the existing public getter, `self::ERROR_EXCEEDED_MAX_BOOKMARKS === $this->get_last_error()`, clears it. That works because PHPStan doesn't carry the property's narrowing over to a method's return value. > > **Recommendation:** add it to your new HTML API ignores section in `phpstan.neon.dist`, with a comment explaining why. That matches the rule written in that file: code that's right as written, where satisfying PHPStan "would mean changing code that has no other reason to change." Switching to `get_last_error()` would work and has no behavior change, but it changes correct code purely to get around a PHPStan limitation --- phpstan.neon.dist | 8 ++++++++ tests/phpstan/baselines/identical.alwaysFalse.neon | 5 ----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5bd77b131f2a5..2b7de4b6e8689 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -182,3 +182,11 @@ parameters: identifier: catch.neverThrown count: 1 path: src/wp-includes/html-api/class-wp-html-processor.php + - + # The early return at the start of step() narrows $last_error to null, and PHPStan does not see that + # bookmark_token() sets it to ERROR_EXCEEDED_MAX_BOOKMARKS before throwing. So it reports that the + # check in the catch block can never match, when in fact it always does. + message: '#^Strict comparison using \=\=\= between ''exceeded\-max…'' and null will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: src/wp-includes/html-api/class-wp-html-processor.php diff --git a/tests/phpstan/baselines/identical.alwaysFalse.neon b/tests/phpstan/baselines/identical.alwaysFalse.neon index 53a42542a0410..7c086f0695a0b 100644 --- a/tests/phpstan/baselines/identical.alwaysFalse.neon +++ b/tests/phpstan/baselines/identical.alwaysFalse.neon @@ -23,11 +23,6 @@ parameters: identifier: identical.alwaysFalse count: 1 path: ../../../src/wp-admin/plugins.php - - - message: '#^Strict comparison using \=\=\= between ''exceeded\-max…'' and null will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: ../../../src/wp-includes/html-api/class-wp-html-processor.php - message: '#^Strict comparison using \=\=\= between 3000000000 and 2147483647 will always evaluate to false\.$#' identifier: identical.alwaysFalse From a5dab1b179af54cd0f65c58fa0cb350d6d59fda3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 9 Oct 2026 11:00:54 -0700 Subject: [PATCH 8/8] Restore remaining bail --- phpstan.neon.dist | 2 +- src/wp-includes/html-api/class-wp-html-processor.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2b7de4b6e8689..9fd68af62cadc 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -170,7 +170,7 @@ parameters: - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable - count: 14 + count: 15 path: src/wp-includes/html-api/class-wp-html-processor.php - message: '#^If condition is always true\.$#' diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 7f9759c10cd30..f6d0d548223b5 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -3356,6 +3356,8 @@ private function step_in_body(): bool { */ return $this->in_body_any_other_end_tag(); } + + $this->bail( 'Should not have been able to reach end of IN BODY processing. Check HTML API code.' ); } /**