Fix compatibility false positive on function_exists guarded calls#1364
Merged
Conversation
WP_Functions_Compatibility_Check tokenizes function calls without any control flow analysis, so a call guarded by function_exists() was still reported as incompatible with the plugin's minimum WordPress version. That guard is a documented backward compatibility pattern, so the call never runs on versions that lack the function. Collect function names guarded by a function_exists( 'name' ) literal and suppress compatibility findings for those names within the same file. This also covers the deferred hook case where the guard and the call live in different methods, which the reported example uses. Add a test fixture and case for the guarded pattern. Closes #1363
Extract the function_exists() argument resolution into its own method so neither method exceeds the PHPMD NPath complexity threshold. No behavior change; the guarded-call suppression works exactly as before.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
davidperezgar
approved these changes
Jun 17, 2026
ernilambar
approved these changes
Jun 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Closes #1363
WP_Functions_Compatibility_Checkno longer reports a function call as incompatible with the plugin's minimum WordPress version when that function is guarded by afunction_exists()check in the same file.Why?
The check finds calls by tokenizing the file with
token_get_all()and matching anyT_STRINGfollowed by(. It has no control flow analysis, so it cannot see that a call is protected by afunction_exists()guard. That produces a false positive for a documented backward compatibility pattern: the guarded call never runs on WordPress versions that lack the function.The reported example guards in
__construct()and calls in a separateadd()method wired throughadd_action(), so the guard and the call live in different methods. A narrow "guard wraps the call" rule would not catch that case, which is why this works at the file level.How?
A new pass collects every function name guarded by a
function_exists( 'name' )string literal in the file, then the call scan skips any call whose name is in that set. Only string literal arguments are trusted, since a dynamic argument cannot be resolved to a name through tokenization. Matching is case-insensitive and tolerates a leading namespace separator.The trade off is a rare false negative if a file guards a function in one place but also calls it unconditionally elsewhere. That is the safe direction for a linter, since a false positive pushes people to disable the whole check. For reference, PHPCompatibility's equivalent sniff does not honor
function_exists()guards either, so this is more forgiving than that baseline.Testing Instructions
Requires at least: 4.9.6in its header.wp plugin check your-plugin --checks=wp_functions_compatibility.wp_function_not_compatible_with_requires_wpreported on theregister_block_type()line.function_exists()guard and run the check again. Expected: the error returns, confirming unguarded calls are still flagged.The PR adds a
test-plugin-wp-functions-compatibility-with-function-exists-guardfixture and atest_run_with_function_exists_guard_without_errorscase covering the guarded pattern.AI Usage Disclosure
If AI tools were used, please describe how they were used:
Changelog entry