Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,9 @@ private function find_wp_function_calls( string $file ): array {
return array();
}

$tokens = token_get_all( $source );
$calls = array();
$tokens = token_get_all( $source );
$guarded_functions = $this->get_function_exists_guarded_names( $tokens );
$calls = array();

foreach ( $tokens as $index => $token ) {
if ( ! is_array( $token ) || T_STRING !== $token[0] ) {
Expand All @@ -269,15 +270,88 @@ private function find_wp_function_calls( string $file ): array {
continue;
}

$function_name = strtolower( $token[1] );

// Skip calls that are guarded by a function_exists() check for the same
// function anywhere in the file. This is a documented backward
// compatibility pattern, so the call is never reached on WordPress
// versions that lack the function.
if ( isset( $guarded_functions[ $function_name ] ) ) {
continue;
}

$calls[] = array(
'name' => strtolower( $token[1] ),
'name' => $function_name,
'line' => (int) $token[2],
);
}

return $calls;
}

/**
* Collects function names guarded by a function_exists() call within the file.
*
* Only string literal arguments are considered, since a dynamic argument cannot
* be resolved to a specific function name through tokenization.
*
* @since 2.0.0
*
* @param array $tokens Token stream.
* @return array<string, true> Map of guarded function names, keyed by lowercase name.
*/
private function get_function_exists_guarded_names( array $tokens ): array {
$guarded = array();

foreach ( $tokens as $index => $token ) {
if ( ! is_array( $token ) || T_STRING !== $token[0] || 'function_exists' !== strtolower( $token[1] ) ) {
continue;
}

$guarded_name = $this->resolve_function_exists_argument( $tokens, $index );
if ( null !== $guarded_name ) {
$guarded[ $guarded_name ] = true;
}
}

return $guarded;
}

/**
* Resolves the string literal argument of a function_exists() call to a function name.
*
* @since 2.0.0
*
* @param array $tokens Token stream.
* @param int $index Index of the function_exists T_STRING token.
* @return string|null Lowercase function name, or null when the call is not a
* global function_exists() with a single string literal argument.
*/
private function resolve_function_exists_argument( array $tokens, int $index ): ?string {
$open_index = $this->get_next_significant_token_index( $tokens, $index );
if ( null === $open_index || '(' !== $tokens[ $open_index ] ) {
return null;
}

if ( ! $this->is_global_function_call( $tokens, $index ) ) {
return null;
}

$argument_index = $this->get_next_significant_token_index( $tokens, $open_index );
if ( null === $argument_index ) {
return null;
}

$argument_token = $tokens[ $argument_index ];
if ( ! is_array( $argument_token ) || T_CONSTANT_ENCAPSED_STRING !== $argument_token[0] ) {
return null;
}

$guarded_name = strtolower( ltrim( trim( $argument_token[1], '\'"' ), '\\' ) );

return '' !== $guarded_name ? $guarded_name : null;
}

/**
* Checks whether a tokenized T_STRING is a global function call.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
/**
* Plugin Name: Test Plugin WP Functions Compatibility With function_exists Guard
* Plugin URI: https://github.com/WordPress/plugin-check
* Description: Test plugin for WordPress functions compatibility check where a newer function is guarded by function_exists().
* Requires at least: 5.0
* Requires PHP: 7.4
* Tested up to: 5.4
* Version: 1.0.0
* Author: WordPress Performance Team
* Author URI: https://make.wordpress.org/performance/
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: test-plugin-wp-functions-compatibility-with-function-exists-guard
*
* @package test-plugin-wp-functions-compatibility-with-function-exists-guard
*/

require_once __DIR__ . '/uses-guarded-function.php';
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
/**
* Test file with a newer WordPress function guarded by function_exists().
*
* The guard and the call live in different methods of the same file, wired
* together through add_action(), mirroring the real-world deferred hook pattern.
*
* @package test-plugin-wp-functions-compatibility-with-function-exists-guard
*/

class Guarded_Feature {

public function __construct() {
if ( function_exists( 'wp_get_environment_type' ) ) {
add_action( 'init', array( $this, 'run' ) );
}
}

public function run() {
wp_get_environment_type();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ public function test_run_without_errors() {
$this->assertEmpty( wp_list_filter( $errors['uses-compatible-function.php'][8][0] ?? array(), array( 'code' => 'wp_function_not_compatible_with_requires_wp' ) ) );
}

public function test_run_with_function_exists_guard_without_errors() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-wp-functions-compatibility-with-function-exists-guard/load.php' );
$check_result = new Check_Result( $check_context );

$check = new WP_Functions_Compatibility_Check();
$check->run( $check_result );

$errors = $check_result->get_errors();
$this->assertArrayNotHasKey( 'uses-guarded-function.php', $errors );
}

public function test_run_with_php_serialize_without_errors() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-wp-functions-compatibility-with-php-serialize/load.php' );
$check_result = new Check_Result( $check_context );
Expand Down
Loading