Skip to content

Require every if/else path to return in AlwaysReturnInFilter - #909

Merged
GaryJones merged 2 commits into
developfrom
GaryJones/always-return-all-paths
Oct 10, 2026
Merged

GaryJones merged 2 commits into
developfrom
GaryJones/always-return-all-paths

Conversation

@GaryJones

@GaryJones GaryJones commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

This follows on from #907 and fixes two remaining gaps in how AlwaysReturnInFilter decides whether a filter callback always returns a value.

The sniff only treated if as conditional, so a return in an else or elseif counted as one that always runs. A callback that returned only from its else, or from an if/elseif chain without an else, passed even though one path returned nothing:

add_filter( 'my_filter', function( $value ) { // Not flagged.
	if ( is_admin() ) {
		return 'admin';
	} elseif ( is_feed() ) {
		return 'feed';
	}
} );

The sniff now follows each path through if, elseif and else chains, written with braces, as else if, or with the alternative syntax. A chain only counts as returning when it ends in an else and every branch returns, and that applies to chains nested in a branch too. Other control structures, such as loops, switch and try, keep the previous leniency: a return anywhere inside them counts. jrfnl raised the wider version of this on #520, which also covers checking every path through those structures. This PR deals with if chains only.

The second commit fixes the check behind the terminating statement warning added for #719. An exit, die or throw anywhere in the callback's body turned the missing return error into that softer warning, even when it sat in a nested closure or arrow function, such as a shutdown action. The callback itself never terminates there, so this is now the error again.

Both changes can report errors on code that passed before: callbacks with a path that returns nothing, and callbacks whose only throw or exit is in a nested closure. That is worth a line in the changelog. I ran the old and new sniff over the 21 Automattic plugins I have locally and the results did not change, but this sniff only reported four issues across them, so that is a small sample.

AlwaysReturnInFilter treated only an if as conditional, so a return in
an else or elseif counted as one that always runs. A callback that
returned only from its else, or from an if/elseif chain with no else,
passed even though one path returned nothing.

An if chain now only counts as returning when it ends in an else and
every branch returns. Other control structures, such as loops, switch
and try, keep the previous leniency, where any return inside them
counts.
An exit, die or throw anywhere in a filter callback's body turned the
missing return error into the softer terminating statement warning,
even when it sat in a nested closure or arrow function, such as a
shutdown action. The callback itself never terminates there, so it
still fails to return a value on that path.
@GaryJones
GaryJones requested a review from a team as a code owner October 10, 2026 16:29
@GaryJones GaryJones added this to the 3.x milestone Oct 10, 2026
@GaryJones GaryJones self-assigned this Oct 10, 2026
@GaryJones
GaryJones merged commit 6ce6ddd into develop Oct 10, 2026
26 checks passed
@GaryJones
GaryJones deleted the GaryJones/always-return-all-paths branch October 10, 2026 16:34
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