From 1da9c43e1ef6f51dcb3f8f1530c66570bea59fd0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 2 Jul 2023 01:59:48 +0200 Subject: [PATCH] NamingConventions/PrefixAllGlobals: allow non-prefixed declarations for pluggable functions and classes While the `PrefixAllGlobals` sniff already allowed for WP native constants which can be user declared, it did not allow for explicitly pluggable functions and classes being declared in plugins/themes. I'm quite surprised actually that this was not reported as a bug so far. Either way, fixed now. In both cases, a case-insensitive name comparison is done as PHP natively treats function names and class names case-insensitively. The lists added are based on some prelim sniffs created for issue 1803 and WP Core 6.3-beta2. Notes: * When generating these lists, I've explicitly excluded the `wp-includes/compat.php` file (which contains the polyfills for PHP native functionality). * I've intentionally included the Twenty* theme related functions and classes as WPCS should also allow for being used by child themes of those themes. * I've _excluded_ polyfills for new WP functionality from the Twenty* themes (which allow the themes to use those functions when running on older WP versions), as those functions aren't intentionally pluggable by WP itself. * I've marked a number of functions/classes with a `// Deprecated` comment based on manual verification (and the declaration being in the `wp-includes/pluggable-deprecated.php` file). While it would, of course, be better if those would not be "plugged", it is not for this sniff to have an opinion on that and declaring these without a prefix should be allowed. * In the `$pluggable_functions` list, I've intentionally commented two functions out as those are declared nested within another function and do not look _intentionally_ pluggable. This may need further verification, but at least this way it is documented why these are not included in the list. Includes tests. --- .../PrefixAllGlobalsSniff.php | 209 ++++++++++++++++++ .../PrefixAllGlobalsUnitTest.1.inc | 22 ++ 2 files changed, 231 insertions(+) diff --git a/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php b/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php index e02a2ee650..93bea5c923 100644 --- a/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php +++ b/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php @@ -195,6 +195,201 @@ final class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { 'WP_DEFAULT_THEME' => true, ); + /** + * A list of functions declared in WP core as "Pluggable", i.e. overloadable from a plugin. + * + * Note: deprecated functions should still be included in this list as plugins may support older WP versions. + * + * @since 3.0.0. + * + * @var array + */ + protected $pluggable_functions = array( + 'auth_redirect' => true, + 'cache_users' => true, + 'check_admin_referer' => true, + 'check_ajax_referer' => true, + 'get_avatar' => true, + 'get_currentuserinfo' => true, // Deprecated. + 'get_user_by' => true, + 'get_user_by_email' => true, // Deprecated. + 'get_userdata' => true, + 'get_userdatabylogin' => true, // Deprecated. + 'graceful_fail' => true, + 'install_global_terms' => true, + 'install_network' => true, + 'is_user_logged_in' => true, + // 'lowercase_octets' => true, => unclear if this function is meant to be publicly pluggable. + 'maybe_add_column' => true, + 'maybe_create_table' => true, + 'set_current_user' => true, // Deprecated. + 'twenty_twenty_one_entry_meta_footer' => true, + 'twenty_twenty_one_post_thumbnail' => true, + 'twenty_twenty_one_post_title' => true, + 'twenty_twenty_one_posted_by' => true, + 'twenty_twenty_one_posted_on' => true, + 'twenty_twenty_one_setup' => true, + 'twenty_twenty_one_the_posts_navigation' => true, + 'twentyeleven_admin_header_image' => true, + 'twentyeleven_admin_header_style' => true, + 'twentyeleven_comment' => true, + 'twentyeleven_content_nav' => true, + 'twentyeleven_continue_reading_link' => true, + 'twentyeleven_header_style' => true, + 'twentyeleven_posted_on' => true, + 'twentyeleven_setup' => true, + 'twentyfifteen_comment_nav' => true, + 'twentyfifteen_entry_meta' => true, + 'twentyfifteen_excerpt_more' => true, + 'twentyfifteen_fonts_url' => true, + 'twentyfifteen_get_color_scheme' => true, + 'twentyfifteen_get_color_scheme_choices' => true, + 'twentyfifteen_get_link_url' => true, + 'twentyfifteen_header_style' => true, + 'twentyfifteen_post_thumbnail' => true, + 'twentyfifteen_sanitize_color_scheme' => true, + 'twentyfifteen_setup' => true, + 'twentyfifteen_the_custom_logo' => true, + 'twentyfourteen_admin_header_image' => true, + 'twentyfourteen_admin_header_style' => true, + 'twentyfourteen_excerpt_more' => true, + 'twentyfourteen_font_url' => true, + 'twentyfourteen_header_style' => true, + 'twentyfourteen_list_authors' => true, + 'twentyfourteen_paging_nav' => true, + 'twentyfourteen_post_nav' => true, + 'twentyfourteen_post_thumbnail' => true, + 'twentyfourteen_posted_on' => true, + 'twentyfourteen_setup' => true, + 'twentyfourteen_the_attached_image' => true, + 'twentynineteen_comment_count' => true, + 'twentynineteen_comment_form' => true, + 'twentynineteen_discussion_avatars_list' => true, + 'twentynineteen_entry_footer' => true, + 'twentynineteen_get_user_avatar_markup' => true, + 'twentynineteen_post_thumbnail' => true, + 'twentynineteen_posted_by' => true, + 'twentynineteen_posted_on' => true, + 'twentynineteen_setup' => true, + 'twentynineteen_the_posts_navigation' => true, + 'twentyseventeen_edit_link' => true, + 'twentyseventeen_entry_footer' => true, + 'twentyseventeen_fonts_url' => true, + 'twentyseventeen_header_style' => true, + 'twentyseventeen_posted_on' => true, + 'twentyseventeen_time_link' => true, + 'twentysixteen_categorized_blog' => true, + 'twentysixteen_entry_date' => true, + 'twentysixteen_entry_meta' => true, + 'twentysixteen_entry_taxonomies' => true, + 'twentysixteen_excerpt' => true, + 'twentysixteen_excerpt_more' => true, + 'twentysixteen_fonts_url' => true, + 'twentysixteen_get_color_scheme' => true, + 'twentysixteen_get_color_scheme_choices' => true, + 'twentysixteen_header_style' => true, + 'twentysixteen_post_thumbnail' => true, + 'twentysixteen_sanitize_color_scheme' => true, + 'twentysixteen_setup' => true, + 'twentysixteen_the_custom_logo' => true, + 'twentyten_admin_header_style' => true, + 'twentyten_comment' => true, + 'twentyten_continue_reading_link' => true, + 'twentyten_posted_in' => true, + 'twentyten_posted_on' => true, + 'twentyten_setup' => true, + 'twentythirteen_entry_date' => true, + 'twentythirteen_entry_meta' => true, + 'twentythirteen_excerpt_more' => true, + 'twentythirteen_fonts_url' => true, + 'twentythirteen_paging_nav' => true, + 'twentythirteen_post_nav' => true, + 'twentythirteen_the_attached_image' => true, + 'twentytwelve_comment' => true, + 'twentytwelve_content_nav' => true, + 'twentytwelve_entry_meta' => true, + 'twentytwelve_get_font_url' => true, + 'twentytwenty_customize_partial_blogdescription' => true, + 'twentytwenty_customize_partial_blogname' => true, + 'twentytwenty_customize_partial_site_logo' => true, + 'twentytwenty_generate_css' => true, + 'twentytwenty_get_customizer_css' => true, + 'twentytwenty_get_theme_svg' => true, + 'twentytwenty_the_theme_svg' => true, + 'twentytwentytwo_styles' => true, + 'twentytwentytwo_support' => true, + 'wp_authenticate' => true, + 'wp_cache_add_multiple' => true, + 'wp_cache_delete_multiple' => true, + 'wp_cache_flush_group' => true, + 'wp_cache_flush_runtime' => true, + 'wp_cache_get_multiple' => true, + 'wp_cache_set_multiple' => true, + 'wp_cache_supports' => true, + 'wp_check_password' => true, + 'wp_clear_auth_cookie' => true, + 'wp_clearcookie' => true, // Deprecated. + 'wp_create_nonce' => true, + 'wp_generate_auth_cookie' => true, + 'wp_generate_password' => true, + 'wp_get_cookie_login' => true, // Deprecated. + 'wp_get_current_user' => true, + // 'wp_handle_upload_error' => true, => unclear if this function is meant to be publicly pluggable. + 'wp_hash' => true, + 'wp_hash_password' => true, + 'wp_install' => true, + 'wp_install_defaults' => true, + 'wp_login' => true, // Deprecated. + 'wp_logout' => true, + 'wp_mail' => true, + 'wp_new_blog_notification' => true, + 'wp_new_user_notification' => true, + 'wp_nonce_tick' => true, + 'wp_notify_moderator' => true, + 'wp_notify_postauthor' => true, + 'wp_parse_auth_cookie' => true, + 'wp_password_change_notification' => true, + 'wp_rand' => true, + 'wp_redirect' => true, + 'wp_safe_redirect' => true, + 'wp_salt' => true, + 'wp_sanitize_redirect' => true, + 'wp_set_auth_cookie' => true, + 'wp_set_current_user' => true, + 'wp_set_password' => true, + 'wp_setcookie' => true, // Deprecated. + 'wp_text_diff' => true, + 'wp_upgrade' => true, + 'wp_validate_auth_cookie' => true, + 'wp_validate_redirect' => true, + 'wp_verify_nonce' => true, + ); + + /** + * A list of classes declared in WP core as "Pluggable", i.e. overloadable from a plugin. + * + * Source: {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/pluggable.php} + * and {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/pluggable-deprecated.php} + * + * Note: deprecated classes should still be included in this list as plugins may support older WP versions. + * + * @since 3.0.0. + * + * @var array + */ + protected $pluggable_classes = array( + 'TwentyTwenty_Customize' => true, + 'TwentyTwenty_Non_Latin_Languages' => true, + 'TwentyTwenty_SVG_Icons' => true, + 'TwentyTwenty_Script_Loader' => true, + 'TwentyTwenty_Separator_Control' => true, + 'TwentyTwenty_Walker_Comment' => true, + 'TwentyTwenty_Walker_Page' => true, + 'Twenty_Twenty_One_Customize' => true, + 'WP_User_Search' => true, + 'wp_atom_server' => true, // Deprecated. + ); + /** * List of all PHP native functions. * @@ -220,6 +415,10 @@ public function register() { $this->built_in_functions = array_flip( $all_functions['internal'] ); $this->built_in_functions = array_change_key_case( $this->built_in_functions, \CASE_LOWER ); + // Make sure the pluggable functions and classes list can be easily compared. + $this->pluggable_functions = array_change_key_case( $this->pluggable_functions, \CASE_LOWER ); + $this->pluggable_classes = array_change_key_case( $this->pluggable_classes, \CASE_LOWER ); + // Set the sniff targets. $targets = array( \T_NAMESPACE => \T_NAMESPACE, @@ -420,6 +619,11 @@ public function process_token( $stackPtr ) { return; } + if ( isset( $this->pluggable_functions[ $item_lc ] ) ) { + // Pluggable function should not be prefixed. + return; + } + $error_text = 'Functions declared in the global namespace'; $error_code = 'NonPrefixedFunctionFound'; break; @@ -434,6 +638,11 @@ public function process_token( $stackPtr ) { switch ( $this->tokens[ $stackPtr ]['code'] ) { case \T_CLASS: + if ( isset( $this->pluggable_classes[ strtolower( $item_name ) ] ) ) { + // Pluggable class should not be prefixed. + return; + } + if ( class_exists( '\\' . $item_name, false ) ) { // Backfill for PHP native class. return; diff --git a/WordPress/Tests/NamingConventions/PrefixAllGlobalsUnitTest.1.inc b/WordPress/Tests/NamingConventions/PrefixAllGlobalsUnitTest.1.inc index fedc9cce9c..6da047a248 100644 --- a/WordPress/Tests/NamingConventions/PrefixAllGlobalsUnitTest.1.inc +++ b/WordPress/Tests/NamingConventions/PrefixAllGlobalsUnitTest.1.inc @@ -651,4 +651,26 @@ if ( function_exists( 'stripos' ) ) { function striPos() {} } +/* + * Safeguard that pluggable functions and classes can be declared without a prefix. + */ +function wp_hash_password( $password ) { + // Do something. + return $hash; +} + +function WP_Mail() {} + +class WP_User_Search {} + +class WP_Atom_Server { + public function __call( $name, $arguments ) { + // Do something. + } + + public static function __callStatic( $name, $arguments ) { + // Do something. + } +} + // phpcs:set WordPress.NamingConventions.PrefixAllGlobals prefixes[]