Skip to content

Fix compatibility false positive on function_exists guarded calls#1364

Merged
davidperezgar merged 2 commits into
trunkfrom
fix/1363-function-exists-guard
Jun 18, 2026
Merged

Fix compatibility false positive on function_exists guarded calls#1364
davidperezgar merged 2 commits into
trunkfrom
fix/1363-function-exists-guard

Conversation

@shameemreza

@shameemreza shameemreza commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What?

Closes #1363

WP_Functions_Compatibility_Check no longer reports a function call as incompatible with the plugin's minimum WordPress version when that function is guarded by a function_exists() check in the same file.

Why?

The check finds calls by tokenizing the file with token_get_all() and matching any T_STRING followed by (. It has no control flow analysis, so it cannot see that a call is protected by a function_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 separate add() method wired through add_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

  1. Create a plugin with Requires at least: 4.9.6 in its header.
  2. Add a class that guards a newer function and calls it from a hooked method:
class Guarded_Feature {
	public function __construct() {
		if ( function_exists( 'register_block_type' ) ) {
			add_action( 'init', array( \$this, 'run' ) );
		}
	}
	public function run() {
		register_block_type( 'my-plugin/gallery', array() );
	}
}
  1. Run wp plugin check your-plugin --checks=wp_functions_compatibility.
  2. Expected: no errors. Previously: wp_function_not_compatible_with_requires_wp reported on the register_block_type() line.
  3. Remove the 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-guard fixture and a test_run_with_function_exists_guard_without_errors case covering the guarded pattern.

AI Usage Disclosure

  • This PR was created without the help of AI tools.
  • This PR includes AI-assisted code or content.

If AI tools were used, please describe how they were used:

Changelog entry

Fix - False positive in WordPress function compatibility check when the call is guarded by function_exists()

Open WordPress Playground Preview

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.
@shameemreza shameemreza marked this pull request as ready for review June 17, 2026 09:10
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: shameemreza <shameemreza@git.wordpress.org>
Co-authored-by: davidperezgar <davidperez@git.wordpress.org>
Co-authored-by: ernilambar <nilambar@git.wordpress.org>
Co-authored-by: marekdedic <marekdedic@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@davidperezgar davidperezgar merged commit cb1a134 into trunk Jun 18, 2026
29 checks passed
@davidperezgar davidperezgar deleted the fix/1363-function-exists-guard branch June 18, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wp_function_not_compatible_with_requires_wp false positive when call is guarded by function_exists()

3 participants