Skip to content

Only check a filter callback's own returns in AlwaysReturnInFilter - #907

Merged
GaryJones merged 1 commit into
developfrom
GaryJones/always-return-nested-scopes
Oct 10, 2026
Merged

GaryJones merged 1 commit into
developfrom
GaryJones/always-return-nested-scopes

Conversation

@GaryJones

Copy link
Copy Markdown
Contributor

AlwaysReturnInFilter looked at every return in a filter callback's body, including returns that belong to a nested closure or an anonymous class method. It also treated a return as conditional whenever there was an if anywhere above it, even an if outside the callback. That gave two false positive errors and one false negative.

An intentional return; in a nested closure was reported as the filter returning void:

add_filter( 'the_content', function( $content ) {
	add_action( 'wp_footer', function() {
		return;
	} );
	return $content; // VoidReturn error on the filter.
} );

A callback wrapped in a function_exists() check that returns only from try and catch was reported as missing a return. The same function without the wrapper passed:

if ( ! function_exists( 'my_filter' ) ) {
	function my_filter( $value ) { // MissingReturnStatement error.
		try {
			return compute( $value );
		} catch ( Exception $e ) {
			return $value;
		}
	}
}
add_filter( 'my_filter', 'my_filter' );

A callback that never returns passed if a nested closure inside it did:

add_filter( 'my_filter', function( $items ) { // Not flagged.
	$items = array_map( function( $item ) {
		return $item;
	}, $items );
} );

The sniff now skips any return whose nearest enclosing function or closure is not the callback, using Conditions::getLastCondition() from PHPCSUtils. When it decides whether a return is conditional, it only counts if statements inside the callback.

The last case means code that passed before can now get a MissingReturnStatement error. That is the correct result, but it may be worth a line in the changelog.

Two related gaps are left for another time. A return that appears only in an else still counts as unconditional, because the sniff only treats if as a control structure (see #520). hasTerminatingStatement() also still sees an exit or throw inside a nested closure. That only changes whether a callback with no direct return gets the warning or the error.

This came up while reviewing #904 and #905.

AlwaysReturnInFilter looked at every return in the callback body,
including those in nested closures and anonymous class methods. A
callback that never returned passed when a nested closure did, and an
intentional `return;` in a nested closure, such as an add_action()
callback, was reported as the filter returning void.

It also treated a return as conditional when an if wrapped the whole
callback. A function inside a function_exists() check that returned
only from try and catch was reported as missing a return, while the
same function without the wrapper passed.
@GaryJones
GaryJones requested a review from a team as a code owner October 9, 2026 21:43
@GaryJones GaryJones added this to the 3.x milestone Oct 9, 2026
@GaryJones GaryJones self-assigned this Oct 9, 2026
@GaryJones
GaryJones merged commit 53ce623 into develop Oct 10, 2026
26 checks passed
@GaryJones
GaryJones deleted the GaryJones/always-return-nested-scopes branch October 10, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant