From fca685019eef83f1e6e2027b392cadf863d0a3e9 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Fri, 2 Oct 2026 16:18:20 +0100 Subject: [PATCH 1/3] fix: find an early is_main_query() return anywhere in its if The search for the return ran in local mode, which stops at the first statement terminator, so a return after any other statement was missed and the rest of the callback was flagged. It now accepts a return that belongs to the if itself. A return inside a nested condition or closure no longer counts, because it does not stop the other queries from reaching the code below. The old search accepted those when they came first. --- .../Sniffs/Hooks/PreGetPostsSniff.php | 20 +++++++------- .../Tests/Hooks/PreGetPostsUnitTest.inc | 27 +++++++++++++++++++ .../Tests/Hooks/PreGetPostsUnitTest.php | 2 ++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php index 716889b9..b3950f87 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php @@ -356,16 +356,18 @@ private function isEarlyMainQueryCheck( $stackPtr ): bool { return false; } - $next = $this->phpcsFile->findNext( - [ T_RETURN ], - $this->tokens[ $this->tokens[ $nestedParenthesisEnd ]['parenthesis_owner'] ]['scope_opener'], - $this->tokens[ $this->tokens[ $nestedParenthesisEnd ]['parenthesis_owner'] ]['scope_closer'], - false, - 'return', - true - ); + // Look for a return which belongs to the control structure itself, not to anything nested inside it. + $scopeCloser = $this->tokens[ $owner ]['scope_closer']; + $return = $this->phpcsFile->findNext( T_RETURN, $this->tokens[ $owner ]['scope_opener'] + 1, $scopeCloser ); + while ( $return !== false ) { + if ( array_key_last( $this->tokens[ $return ]['conditions'] ) === $owner ) { + return true; + } + + $return = $this->phpcsFile->findNext( T_RETURN, $return + 1, $scopeCloser ); + } - return $next !== false; + return false; } /** diff --git a/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc index 5cb8c0d4..31f6082c 100644 --- a/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc @@ -146,3 +146,30 @@ class short_array_hook_in { } } } + +// The early return does not have to be the first statement in the if. See #902. +add_action( 'pre_get_posts', function( $query ) { + if ( ! $query->is_main_query() ) { + do_log( 'x' ); + return; + } + $query->set( 'cat', '-5' ); // Ok. +} ); + +add_action( 'pre_get_posts', function( $query ) { + if ( ! $query->is_main_query() ) { + if ( is_admin() ) { + return; + } + } + $query->set( 'cat', '-5' ); // Warning - the return is conditional. +} ); + +add_action( 'pre_get_posts', function( $query ) { + if ( ! $query->is_main_query() ) { + $callback = function() { + return; + }; + } + $query->set( 'cat', '-5' ); // Warning - the return belongs to the nested closure. +} ); diff --git a/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.php index 363f3244..6598375a 100644 --- a/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.php @@ -41,6 +41,8 @@ public function getWarningList() { 87 => 1, 128 => 1, 133 => 1, + 165 => 1, + 174 => 1, ]; } } From 5b1aef620f9f61d42121fb619820341c58cb415a Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Fri, 2 Oct 2026 16:22:58 +0100 Subject: [PATCH 2/3] fix: avoid array_key_last() in the early return check PHPStan is configured to analyse as PHP 7.1, where array_key_last() does not exist. Use the array_keys() and array_pop() pattern the sniff already uses elsewhere. --- WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php index b3950f87..afa81d6c 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php @@ -360,7 +360,8 @@ private function isEarlyMainQueryCheck( $stackPtr ): bool { $scopeCloser = $this->tokens[ $owner ]['scope_closer']; $return = $this->phpcsFile->findNext( T_RETURN, $this->tokens[ $owner ]['scope_opener'] + 1, $scopeCloser ); while ( $return !== false ) { - if ( array_key_last( $this->tokens[ $return ]['conditions'] ) === $owner ) { + $conditionStackPtrs = array_keys( $this->tokens[ $return ]['conditions'] ); + if ( array_pop( $conditionStackPtrs ) === $owner ) { return true; } From 1e3d29e9b0a489c41a9dbc2f42ce64a0f6693a87 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Fri, 9 Oct 2026 22:06:53 +0100 Subject: [PATCH 3/3] test: cover an early return in alternative syntax The old search for the early return also missed it in an if written with alternative syntax, when the return was not the first statement. The new search finds it there too, so this pins that down. --- WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc index 31f6082c..33f7416b 100644 --- a/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/PreGetPostsUnitTest.inc @@ -173,3 +173,11 @@ add_action( 'pre_get_posts', function( $query ) { } $query->set( 'cat', '-5' ); // Warning - the return belongs to the nested closure. } ); + +add_action( 'pre_get_posts', function( $query ) { + if ( ! $query->is_main_query() ) : + do_log( 'x' ); + return; + endif; + $query->set( 'cat', '-5' ); // Ok - alternative syntax. +} );