1287 lines
45 KiB
PHP
1287 lines
45 KiB
PHP
<?php
|
|
/**
|
|
* WordPress Coding Standard.
|
|
*
|
|
* @package WPCS\WordPressCodingStandards
|
|
* @link https://github.com/WordPress/WordPress-Coding-Standards
|
|
* @license https://opensource.org/licenses/MIT MIT
|
|
*/
|
|
|
|
namespace WordPressCS\WordPress\Sniffs\NamingConventions;
|
|
|
|
use PHP_CodeSniffer\Util\Tokens;
|
|
use PHPCSUtils\BackCompat\Helper;
|
|
use PHPCSUtils\Tokens\Collections;
|
|
use PHPCSUtils\Utils\Conditions;
|
|
use PHPCSUtils\Utils\Context;
|
|
use PHPCSUtils\Utils\FunctionDeclarations;
|
|
use PHPCSUtils\Utils\Lists;
|
|
use PHPCSUtils\Utils\MessageHelper;
|
|
use PHPCSUtils\Utils\Namespaces;
|
|
use PHPCSUtils\Utils\ObjectDeclarations;
|
|
use PHPCSUtils\Utils\Parentheses;
|
|
use PHPCSUtils\Utils\PassedParameters;
|
|
use PHPCSUtils\Utils\Scopes;
|
|
use PHPCSUtils\Utils\TextStrings;
|
|
use PHPCSUtils\Utils\Variables;
|
|
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
|
|
use WordPressCS\WordPress\Helpers\DeprecationHelper;
|
|
use WordPressCS\WordPress\Helpers\IsUnitTestTrait;
|
|
use WordPressCS\WordPress\Helpers\ListHelper;
|
|
use WordPressCS\WordPress\Helpers\RulesetPropertyHelper;
|
|
use WordPressCS\WordPress\Helpers\VariableHelper;
|
|
use WordPressCS\WordPress\Helpers\WPGlobalVariablesHelper;
|
|
use WordPressCS\WordPress\Helpers\WPHookHelper;
|
|
|
|
/**
|
|
* Verify that everything defined in the global namespace is prefixed with a theme/plugin specific prefix.
|
|
*
|
|
* @since 0.12.0
|
|
* @since 0.13.0 Class name changed: this class is now namespaced.
|
|
* @since 1.2.0 Now also checks whether namespaces are prefixed.
|
|
* @since 2.2.0 - Now also checks variables assigned via the list() construct.
|
|
* - Now also ignores global functions which are marked as @deprecated.
|
|
*
|
|
* @uses \WordPressCS\WordPress\Helpers\IsUnitTestTrait::$custom_test_classes
|
|
*/
|
|
final class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff {
|
|
|
|
use IsUnitTestTrait;
|
|
|
|
/**
|
|
* Error message template.
|
|
*
|
|
* @var string
|
|
*/
|
|
const ERROR_MSG = '%s by a theme/plugin should start with the theme/plugin prefix. Found: "%s".';
|
|
|
|
/**
|
|
* Minimal number of characters the prefix needs in order to be valid.
|
|
*
|
|
* @since 2.2.0
|
|
*
|
|
* @link https://github.com/WordPress/WordPress-Coding-Standards/issues/1733 Issue 1733.
|
|
*
|
|
* @var int
|
|
*/
|
|
const MIN_PREFIX_LENGTH = 3;
|
|
|
|
/**
|
|
* Target prefixes.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @var string[]
|
|
*/
|
|
public $prefixes = array();
|
|
|
|
/**
|
|
* Prefix blocklist.
|
|
*
|
|
* @since 0.12.0
|
|
* @since 3.0.0 Renamed from `$prefix_blacklist` to `$prefix_blocklist`.
|
|
*
|
|
* @var array<string, true> Key is prefix, value irrelevant.
|
|
*/
|
|
protected $prefix_blocklist = array(
|
|
'wordpress' => true,
|
|
'wp' => true,
|
|
'_' => true,
|
|
'php' => true, // See #1728, the 'php' prefix is reserved by PHP itself.
|
|
);
|
|
|
|
/**
|
|
* Target prefixes after validation.
|
|
*
|
|
* All prefixes are lowercased for case-insensitive compare.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @var array<string, string>
|
|
*/
|
|
private $validated_prefixes = array();
|
|
|
|
/**
|
|
* Target namespace prefixes after validation with regex indicator.
|
|
*
|
|
* All prefixes are lowercased for case-insensitive compare.
|
|
* If the prefix doesn't already contain a namespace separator, but does contain
|
|
* non-word characters, these will have been replaced with regex syntax to allow
|
|
* for namespace separators and the `is_regex` indicator will have been set to `true`.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @var array<string, array<string, mixed>>
|
|
*/
|
|
private $validated_namespace_prefixes = array();
|
|
|
|
/**
|
|
* Cache of previously set prefixes.
|
|
*
|
|
* Prevents having to do the same prefix validation over and over again.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private $previous_prefixes = array();
|
|
|
|
/**
|
|
* A list of core hooks that are allowed to be called by plugins and themes.
|
|
*
|
|
* @since 0.14.0
|
|
* @since 3.0.0 Renamed from `$whitelisted_core_hooks` to `$allowed_core_hooks`.
|
|
*
|
|
* @var array<string, true> Key is hook name, value irrelevant.
|
|
*/
|
|
protected $allowed_core_hooks = array(
|
|
'widget_title' => true,
|
|
'add_meta_boxes' => true,
|
|
);
|
|
|
|
/**
|
|
* A list of core constants that are allowed to be defined by plugins and themes.
|
|
*
|
|
* Source: {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/default-constants.php#L0}
|
|
* The constants are listed in alphabetic order.
|
|
* Only overrulable constants are listed, i.e. those defined within core within
|
|
* a `if ( ! defined() ) {}` wrapper.
|
|
*
|
|
* Last update: July 2023 for WP 6.3 at https://github.com/WordPress/wordpress-develop/commit/6281ce432c50345a57768bf53854d9b65b6cdd52
|
|
*
|
|
* @since 1.0.0
|
|
* @since 3.0.0 Renamed from `$whitelisted_core_constants` to `$allowed_core_constants`.
|
|
*
|
|
* @var array<string, true> Key is constant name, value irrelevant.
|
|
*/
|
|
protected $allowed_core_constants = array(
|
|
'ADMIN_COOKIE_PATH' => true,
|
|
'AUTH_COOKIE' => true,
|
|
'AUTOSAVE_INTERVAL' => true,
|
|
'COOKIEHASH' => true,
|
|
'COOKIEPATH' => true,
|
|
'COOKIE_DOMAIN' => true,
|
|
'EMPTY_TRASH_DAYS' => true,
|
|
'FORCE_SSL_ADMIN' => true,
|
|
'FORCE_SSL_LOGIN' => true, // Deprecated.
|
|
'LOGGED_IN_COOKIE' => true,
|
|
'MEDIA_TRASH' => true,
|
|
'MUPLUGINDIR' => true, // Deprecated.
|
|
'PASS_COOKIE' => true,
|
|
'PLUGINDIR' => true, // Deprecated.
|
|
'PLUGINS_COOKIE_PATH' => true,
|
|
'RECOVERY_MODE_COOKIE' => true,
|
|
'SCRIPT_DEBUG' => true,
|
|
'SECURE_AUTH_COOKIE' => true,
|
|
'SHORTINIT' => true,
|
|
'SITECOOKIEPATH' => true,
|
|
'TEST_COOKIE' => true,
|
|
'USER_COOKIE' => true,
|
|
'WPMU_PLUGIN_DIR' => true,
|
|
'WPMU_PLUGIN_URL' => true,
|
|
'WP_CACHE' => true,
|
|
'WP_CONTENT_DIR' => true,
|
|
'WP_CONTENT_URL' => true,
|
|
'WP_CRON_LOCK_TIMEOUT' => true,
|
|
'WP_DEBUG' => true,
|
|
'WP_DEBUG_DISPLAY' => true,
|
|
'WP_DEBUG_LOG' => true,
|
|
'WP_DEFAULT_THEME' => true,
|
|
'WP_DEVELOPMENT_MODE' => true,
|
|
'WP_MAX_MEMORY_LIMIT' => true,
|
|
'WP_MEMORY_LIMIT' => true,
|
|
'WP_PLUGIN_DIR' => true,
|
|
'WP_PLUGIN_URL' => true,
|
|
'WP_POST_REVISIONS' => true,
|
|
'WP_START_TIMESTAMP' => 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<string, true> Key is function name, value irrelevant.
|
|
*/
|
|
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<string, true> Key is class name, value irrelevant.
|
|
*/
|
|
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.
|
|
*
|
|
* Using this list rather than a call to `function_exists()` prevents
|
|
* false negatives from user-defined functions when those would be
|
|
* autoloaded via a Composer autoload files directives.
|
|
*
|
|
* @var array<string, int>
|
|
*/
|
|
private $built_in_functions;
|
|
|
|
|
|
/**
|
|
* Returns an array of tokens this test wants to listen for.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @return array
|
|
*/
|
|
public function register() {
|
|
// Get a list of all PHP native functions.
|
|
$all_functions = get_defined_functions();
|
|
$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,
|
|
\T_FUNCTION => \T_FUNCTION,
|
|
\T_CONST => \T_CONST,
|
|
\T_VARIABLE => \T_VARIABLE,
|
|
\T_DOLLAR => \T_DOLLAR, // Variable variables.
|
|
\T_FN_ARROW => \T_FN_ARROW, // T_FN_ARROW is only used for skipping over (for now).
|
|
);
|
|
$targets += Tokens::$ooScopeTokens; // T_ANON_CLASS is only used for skipping over test classes.
|
|
$targets += Collections::listOpenTokensBC();
|
|
|
|
// Add function call target for hook names and constants defined using define().
|
|
$parent = parent::register();
|
|
if ( ! empty( $parent ) ) {
|
|
$targets[] = \T_STRING;
|
|
}
|
|
|
|
return $targets;
|
|
}
|
|
|
|
/**
|
|
* Groups of functions to restrict.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getGroups() {
|
|
// Only retrieve functions which are not used for deprecated hooks.
|
|
$this->target_functions = WPHookHelper::get_functions( false );
|
|
$this->target_functions['define'] = true;
|
|
|
|
return parent::getGroups();
|
|
}
|
|
|
|
/**
|
|
* Processes this test, when one of its tokens is encountered.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @param int $stackPtr The position of the current token in the stack.
|
|
*
|
|
* @return int|void Integer stack pointer to skip forward or void to continue
|
|
* normal file processing.
|
|
*/
|
|
public function process_token( $stackPtr ) {
|
|
|
|
// Allow overruling the prefixes set in a ruleset via the command line.
|
|
$cl_prefixes = Helper::getConfigData( 'prefixes' );
|
|
if ( ! empty( $cl_prefixes ) ) {
|
|
$cl_prefixes = trim( $cl_prefixes );
|
|
if ( '' !== $cl_prefixes ) {
|
|
$this->prefixes = array_filter( array_map( 'trim', explode( ',', $cl_prefixes ) ) );
|
|
}
|
|
}
|
|
|
|
$this->prefixes = RulesetPropertyHelper::merge_custom_array( $this->prefixes, array(), false );
|
|
if ( empty( $this->prefixes ) ) {
|
|
// No prefixes passed, nothing to do.
|
|
return;
|
|
}
|
|
|
|
$this->validate_prefixes();
|
|
if ( empty( $this->validated_prefixes ) ) {
|
|
// No _valid_ prefixes passed, nothing to do.
|
|
return;
|
|
}
|
|
|
|
// Ignore test classes.
|
|
if ( isset( Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] )
|
|
&& true === $this->is_test_class( $this->phpcsFile, $stackPtr )
|
|
) {
|
|
if ( $this->tokens[ $stackPtr ]['scope_condition'] === $stackPtr && isset( $this->tokens[ $stackPtr ]['scope_closer'] ) ) {
|
|
// Skip forward to end of test class.
|
|
return $this->tokens[ $stackPtr ]['scope_closer'];
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ( \T_ANON_CLASS === $this->tokens[ $stackPtr ]['code'] ) {
|
|
// Token was only registered to allow skipping over test classes.
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Ignore the contents of arrow functions which do not declare closures.
|
|
*
|
|
* - Parameters declared by arrow functions do not need to be prefixed (handled elsewhere).
|
|
* - New variables declared within an arrow function are local to the arrow function, so can be ignored.
|
|
* - A `global` statement is not allowed within an arrow function.
|
|
*
|
|
* Note: this does mean some convoluted code may get ignored (false negatives), but this is currently
|
|
* not reliably solvable as PHPCS does not add arrow functions to the 'conditions' array.
|
|
*/
|
|
if ( \T_FN_ARROW === $this->tokens[ $stackPtr ]['code']
|
|
&& isset( $this->tokens[ $stackPtr ]['scope_closer'] )
|
|
) {
|
|
$has_closure = $this->phpcsFile->findNext( \T_CLOSURE, ( $stackPtr + 1 ), $this->tokens[ $stackPtr ]['scope_closer'] );
|
|
if ( false !== $has_closure ) {
|
|
// Skip to the start of the closure.
|
|
return $has_closure;
|
|
}
|
|
|
|
// Skip the arrow function completely.
|
|
return $this->tokens[ $stackPtr ]['scope_closer'];
|
|
}
|
|
|
|
if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] ) {
|
|
// Disallow excluding function groups for this sniff.
|
|
$this->exclude = array();
|
|
|
|
return parent::process_token( $stackPtr );
|
|
|
|
} elseif ( \T_DOLLAR === $this->tokens[ $stackPtr ]['code'] ) {
|
|
|
|
return $this->process_variable_variable( $stackPtr );
|
|
|
|
} elseif ( \T_VARIABLE === $this->tokens[ $stackPtr ]['code'] ) {
|
|
|
|
return $this->process_variable_assignment( $stackPtr );
|
|
|
|
} elseif ( isset( Collections::listOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) ) {
|
|
return $this->process_list_assignment( $stackPtr );
|
|
|
|
} elseif ( \T_NAMESPACE === $this->tokens[ $stackPtr ]['code'] ) {
|
|
$namespace_name = Namespaces::getDeclaredName( $this->phpcsFile, $stackPtr );
|
|
|
|
if ( false === $namespace_name || '' === $namespace_name || '\\' === $namespace_name ) {
|
|
return;
|
|
}
|
|
|
|
foreach ( $this->validated_namespace_prefixes as $key => $prefix_info ) {
|
|
if ( false === $prefix_info['is_regex'] ) {
|
|
if ( stripos( $namespace_name, $prefix_info['prefix'] ) === 0 ) {
|
|
$this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $key );
|
|
return;
|
|
}
|
|
} else {
|
|
// Ok, so this prefix should be used as a regex.
|
|
$regex = '`^' . $prefix_info['prefix'] . '`i';
|
|
if ( preg_match( $regex, $namespace_name ) > 0 ) {
|
|
$this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $key );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Still here ? In that case, we have a non-prefixed namespace name.
|
|
$recorded = $this->phpcsFile->addError(
|
|
self::ERROR_MSG,
|
|
$stackPtr,
|
|
'NonPrefixedNamespaceFound',
|
|
array(
|
|
'Namespaces declared',
|
|
$namespace_name,
|
|
)
|
|
);
|
|
|
|
if ( true === $recorded ) {
|
|
$this->record_potential_prefix_metric( $stackPtr, $namespace_name );
|
|
}
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
// Namespaced methods, classes and constants do not need to be prefixed.
|
|
$namespace = Namespaces::determineNamespace( $this->phpcsFile, $stackPtr );
|
|
if ( '' !== $namespace && '\\' !== $namespace ) {
|
|
return;
|
|
}
|
|
|
|
$item_name = '';
|
|
$error_text = 'Unknown syntax used';
|
|
$error_code = 'NonPrefixedSyntaxFound';
|
|
|
|
switch ( $this->tokens[ $stackPtr ]['code'] ) {
|
|
case \T_FUNCTION:
|
|
// Methods in a class do not need to be prefixed.
|
|
if ( Scopes::isOOMethod( $this->phpcsFile, $stackPtr ) === true ) {
|
|
return;
|
|
}
|
|
|
|
if ( DeprecationHelper::is_function_deprecated( $this->phpcsFile, $stackPtr ) === true ) {
|
|
/*
|
|
* Deprecated functions don't have to comply with the naming conventions,
|
|
* otherwise functions deprecated in favour of a function with a compliant
|
|
* name would still trigger an error.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
$item_name = FunctionDeclarations::getName( $this->phpcsFile, $stackPtr );
|
|
$item_lc = strtolower( $item_name );
|
|
if ( isset( $this->built_in_functions[ $item_lc ] ) ) {
|
|
// Backfill for PHP native function.
|
|
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;
|
|
|
|
case \T_CLASS:
|
|
case \T_INTERFACE:
|
|
case \T_TRAIT:
|
|
case \T_ENUM:
|
|
$item_name = ObjectDeclarations::getName( $this->phpcsFile, $stackPtr );
|
|
$error_text = 'Classes declared';
|
|
$error_code = 'NonPrefixedClassFound';
|
|
|
|
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;
|
|
}
|
|
break;
|
|
|
|
case \T_INTERFACE:
|
|
if ( interface_exists( '\\' . $item_name, false ) ) {
|
|
// Backfill for PHP native interface.
|
|
return;
|
|
}
|
|
|
|
$error_text = 'Interfaces declared';
|
|
$error_code = 'NonPrefixedInterfaceFound';
|
|
break;
|
|
|
|
case \T_TRAIT:
|
|
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.trait_existsFound
|
|
if ( function_exists( '\trait_exists' ) && trait_exists( '\\' . $item_name, false ) ) {
|
|
// Backfill for PHP native trait.
|
|
return;
|
|
}
|
|
|
|
$error_text = 'Traits declared';
|
|
$error_code = 'NonPrefixedTraitFound';
|
|
break;
|
|
|
|
case \T_ENUM:
|
|
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.enum_existsFound
|
|
if ( function_exists( '\enum_exists' ) && enum_exists( '\\' . $item_name, false ) ) {
|
|
// Backfill for PHP native enum.
|
|
return;
|
|
}
|
|
|
|
$error_text = 'Enums declared';
|
|
$error_code = 'NonPrefixedEnumFound';
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
case \T_CONST:
|
|
// Constants in an OO construct do not need to be prefixed.
|
|
if ( true === Scopes::isOOConstant( $this->phpcsFile, $stackPtr ) ) {
|
|
return;
|
|
}
|
|
|
|
$constant_name_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
|
|
if ( false === $constant_name_ptr ) {
|
|
// Live coding.
|
|
return;
|
|
}
|
|
|
|
$item_name = $this->tokens[ $constant_name_ptr ]['content'];
|
|
if ( \defined( '\\' . $item_name ) ) {
|
|
// Backfill for PHP native constant.
|
|
return;
|
|
}
|
|
|
|
if ( isset( $this->allowed_core_constants[ $item_name ] ) ) {
|
|
// Defining a WP Core constant intended for overruling.
|
|
return;
|
|
}
|
|
|
|
$error_text = 'Global constants defined';
|
|
$error_code = 'NonPrefixedConstantFound';
|
|
break;
|
|
|
|
default:
|
|
// Left empty on purpose.
|
|
break;
|
|
|
|
}
|
|
|
|
if ( empty( $item_name ) || $this->is_prefixed( $stackPtr, $item_name ) === true ) {
|
|
return;
|
|
}
|
|
|
|
$recorded = $this->phpcsFile->addError(
|
|
self::ERROR_MSG,
|
|
$stackPtr,
|
|
$error_code,
|
|
array(
|
|
$error_text,
|
|
$item_name,
|
|
)
|
|
);
|
|
|
|
if ( true === $recorded ) {
|
|
$this->record_potential_prefix_metric( $stackPtr, $item_name );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle variable variables defined in the global namespace.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @param int $stackPtr The position of the current token in the stack.
|
|
*
|
|
* @return int|void Integer stack pointer to skip forward or void to continue
|
|
* normal file processing.
|
|
*/
|
|
protected function process_variable_variable( $stackPtr ) {
|
|
static $indicators = array(
|
|
\T_OPEN_CURLY_BRACKET => true,
|
|
\T_VARIABLE => true,
|
|
);
|
|
|
|
// Is this a variable variable ?
|
|
// Not concerned with nested ones as those will be recognized on their own token.
|
|
$next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
|
|
if ( false === $next_non_empty || ! isset( $indicators[ $this->tokens[ $next_non_empty ]['code'] ] ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( \T_OPEN_CURLY_BRACKET === $this->tokens[ $next_non_empty ]['code']
|
|
&& isset( $this->tokens[ $next_non_empty ]['bracket_closer'] )
|
|
) {
|
|
// Skip over the variable part.
|
|
$next_non_empty = $this->tokens[ $next_non_empty ]['bracket_closer'];
|
|
}
|
|
|
|
$maybe_assignment = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true );
|
|
|
|
while ( false !== $maybe_assignment
|
|
&& \T_OPEN_SQUARE_BRACKET === $this->tokens[ $maybe_assignment ]['code']
|
|
&& isset( $this->tokens[ $maybe_assignment ]['bracket_closer'] )
|
|
) {
|
|
$maybe_assignment = $this->phpcsFile->findNext(
|
|
Tokens::$emptyTokens,
|
|
( $this->tokens[ $maybe_assignment ]['bracket_closer'] + 1 ),
|
|
null,
|
|
true,
|
|
null,
|
|
true
|
|
);
|
|
}
|
|
|
|
if ( false === $maybe_assignment ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! isset( Tokens::$assignmentTokens[ $this->tokens[ $maybe_assignment ]['code'] ] ) ) {
|
|
// Not an assignment.
|
|
return;
|
|
}
|
|
|
|
$error = self::ERROR_MSG;
|
|
|
|
/*
|
|
* Local variable variables in a function do not need to be prefixed.
|
|
* But a variable variable could evaluate to the name of an imported global
|
|
* variable.
|
|
* Not concerned with imported variable variables (global.. ) as that has been
|
|
* forbidden since PHP 7.0. Presuming cross-version code and if not, that
|
|
* is for the PHPCompatibility standard to detect.
|
|
*/
|
|
$functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() );
|
|
if ( false !== $functionPtr ) {
|
|
$has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $functionPtr ]['scope_opener'] );
|
|
if ( false === $has_global ) {
|
|
// No variable import happening.
|
|
return;
|
|
}
|
|
|
|
$error = 'Variable variable which could potentially override an imported global variable detected. ' . $error;
|
|
}
|
|
|
|
$variable_name = $this->phpcsFile->getTokensAsString( $stackPtr, ( ( $next_non_empty - $stackPtr ) + 1 ) );
|
|
|
|
// Still here ? In that case, the variable name should be prefixed.
|
|
$recorded = $this->phpcsFile->addWarning(
|
|
$error,
|
|
$stackPtr,
|
|
'NonPrefixedVariableFound',
|
|
array(
|
|
'Global variables defined',
|
|
$variable_name,
|
|
)
|
|
);
|
|
|
|
if ( true === $recorded ) {
|
|
$this->record_potential_prefix_metric( $stackPtr, $variable_name );
|
|
}
|
|
|
|
// Skip over the variable part of the variable.
|
|
return ( $next_non_empty + 1 );
|
|
}
|
|
|
|
/**
|
|
* Check that defined global variables are prefixed.
|
|
*
|
|
* @since 0.12.0
|
|
* @since 2.2.0 Added $in_list parameter.
|
|
*
|
|
* @param int $stackPtr The position of the current token in the stack.
|
|
* @param bool $in_list Whether or not this is a variable in a list assignment.
|
|
* Defaults to false.
|
|
*
|
|
* @return int|void Integer stack pointer to skip forward or void to continue
|
|
* normal file processing.
|
|
*/
|
|
protected function process_variable_assignment( $stackPtr, $in_list = false ) {
|
|
/*
|
|
* We're only concerned with variables which are being defined.
|
|
* `is_assigment()` will not recognize property assignments, which is good in this case.
|
|
* However it will also not recognize $b in `foreach( $a as $b )` as an assignment, so
|
|
* we need a separate check for that.
|
|
*/
|
|
if ( false === $in_list
|
|
&& false === VariableHelper::is_assignment( $this->phpcsFile, $stackPtr )
|
|
&& Context::inForeachCondition( $this->phpcsFile, $stackPtr ) !== 'afterAs'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
$is_error = true;
|
|
$variable_name = substr( $this->tokens[ $stackPtr ]['content'], 1 ); // Strip the dollar sign.
|
|
|
|
// Bow out early if we know for certain no prefix is needed.
|
|
if ( 'GLOBALS' !== $variable_name
|
|
&& $this->variable_prefixed_or_allowed( $stackPtr, $variable_name ) === true
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if ( 'GLOBALS' === $variable_name ) {
|
|
$array_open = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
|
|
if ( false === $array_open || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $array_open ]['code'] ) {
|
|
// Live coding or something very silly.
|
|
return;
|
|
}
|
|
|
|
$array_key = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $array_open + 1 ), null, true, null, true );
|
|
if ( false === $array_key ) {
|
|
// No key found, nothing to do.
|
|
return;
|
|
}
|
|
|
|
$stackPtr = $array_key;
|
|
$variable_name = TextStrings::stripQuotes( $this->tokens[ $array_key ]['content'] );
|
|
|
|
// Check whether a prefix is needed.
|
|
if ( isset( Tokens::$stringTokens[ $this->tokens[ $array_key ]['code'] ] )
|
|
&& $this->variable_prefixed_or_allowed( $stackPtr, $variable_name ) === true
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $array_key ]['code'] ) {
|
|
// If the array key is a double quoted string, try again with only
|
|
// the part before the first variable (if any).
|
|
$exploded = explode( '$', $variable_name );
|
|
$first = rtrim( $exploded[0], '{' );
|
|
if ( '' !== $first ) {
|
|
if ( $this->variable_prefixed_or_allowed( $array_key, $first ) === true ) {
|
|
return;
|
|
}
|
|
} else {
|
|
// If the first part was dynamic, throw a warning.
|
|
$is_error = false;
|
|
}
|
|
} elseif ( ! isset( Tokens::$stringTokens[ $this->tokens[ $array_key ]['code'] ] ) ) {
|
|
// Dynamic array key, throw a warning.
|
|
$is_error = false;
|
|
}
|
|
} else {
|
|
// Function parameters do not need to be prefixed.
|
|
if ( false === $in_list ) {
|
|
$functionPtr = Parentheses::getLastOwner( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() );
|
|
if ( false !== $functionPtr ) {
|
|
return;
|
|
}
|
|
unset( $functionPtr );
|
|
}
|
|
|
|
// Properties in a class do not need to be prefixed.
|
|
if ( false === $in_list && true === Scopes::isOOProperty( $this->phpcsFile, $stackPtr ) ) {
|
|
return;
|
|
}
|
|
|
|
// Local variables in a function do not need to be prefixed unless they are being imported.
|
|
$functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() );
|
|
if ( false !== $functionPtr ) {
|
|
$has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $functionPtr ]['scope_opener'] );
|
|
if ( false === $has_global
|
|
|| Conditions::getLastCondition( $this->phpcsFile, $has_global, Collections::functionDeclarationTokens() ) !== $functionPtr
|
|
) {
|
|
// No variable import happening in the current scope.
|
|
return;
|
|
}
|
|
|
|
// Ok, this may be an imported global variable.
|
|
$end_of_statement = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), ( $has_global + 1 ) );
|
|
if ( false === $end_of_statement ) {
|
|
// No semi-colon - live coding.
|
|
return;
|
|
}
|
|
|
|
for ( $ptr = ( $has_global + 1 ); $ptr <= $end_of_statement; $ptr++ ) {
|
|
// Move the stack pointer to the next variable.
|
|
$ptr = $this->phpcsFile->findNext( \T_VARIABLE, $ptr, $end_of_statement, false, null, true );
|
|
|
|
if ( false === $ptr ) {
|
|
// Reached the end of the global statement without finding the variable,
|
|
// so this must be a local variable.
|
|
return;
|
|
}
|
|
|
|
if ( substr( $this->tokens[ $ptr ]['content'], 1 ) === $variable_name ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
unset( $has_global, $end_of_statement, $ptr );
|
|
}
|
|
}
|
|
|
|
// Still here ? In that case, the variable name should be prefixed.
|
|
$recorded = MessageHelper::addMessage(
|
|
$this->phpcsFile,
|
|
self::ERROR_MSG,
|
|
$stackPtr,
|
|
$is_error,
|
|
'NonPrefixedVariableFound',
|
|
array(
|
|
'Global variables defined',
|
|
'$' . $variable_name,
|
|
)
|
|
);
|
|
|
|
if ( true === $recorded ) {
|
|
$this->record_potential_prefix_metric( $stackPtr, $variable_name );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that global variables declared via a list construct are prefixed.
|
|
*
|
|
* {@internal No need to take special measures for nested lists. Nested or not,
|
|
* each list part can only contain one variable being written to.}
|
|
*
|
|
* @since 2.2.0
|
|
*
|
|
* @param int $stackPtr The position of the current token in the stack.
|
|
*
|
|
* @return int|void Integer stack pointer to skip forward or void to continue
|
|
* normal file processing.
|
|
*/
|
|
protected function process_list_assignment( $stackPtr ) {
|
|
$list_open_close = Lists::getOpenClose( $this->phpcsFile, $stackPtr );
|
|
if ( false === $list_open_close ) {
|
|
// Short array, not short list.
|
|
return;
|
|
}
|
|
|
|
$var_pointers = ListHelper::get_list_variables( $this->phpcsFile, $stackPtr );
|
|
foreach ( $var_pointers as $ptr ) {
|
|
$this->process_variable_assignment( $ptr, true );
|
|
}
|
|
|
|
// No need to re-examine these variables.
|
|
return $list_open_close['closer'];
|
|
}
|
|
|
|
/**
|
|
* Process the parameters of a matched function.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @param int $stackPtr The position of the current token in the stack.
|
|
* @param string $group_name The name of the group which was matched.
|
|
* @param string $matched_content The token content (function name) which was matched
|
|
* in lowercase.
|
|
* @param array $parameters Array with information about the parameters.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
|
|
if ( 'define' === $matched_content ) {
|
|
$target_param = PassedParameters::getParameterFromStack( $parameters, 1, 'constant_name' );
|
|
|
|
} else {
|
|
$target_param = WPHookHelper::get_hook_name_param( $matched_content, $parameters );
|
|
}
|
|
|
|
if ( false === $target_param ) {
|
|
return;
|
|
}
|
|
|
|
$is_error = true;
|
|
$clean_content = TextStrings::stripQuotes( $target_param['clean'] );
|
|
|
|
if ( ( 'define' !== $matched_content
|
|
&& isset( $this->allowed_core_hooks[ $clean_content ] ) )
|
|
|| ( 'define' === $matched_content
|
|
&& isset( $this->allowed_core_constants[ $clean_content ] ) )
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if ( $this->is_prefixed( $target_param['start'], $clean_content ) === true ) {
|
|
return;
|
|
} else {
|
|
// This may be a dynamic hook/constant name.
|
|
$first_non_empty = $this->phpcsFile->findNext(
|
|
Tokens::$emptyTokens,
|
|
$target_param['start'],
|
|
( $target_param['end'] + 1 ),
|
|
true
|
|
);
|
|
|
|
if ( false === $first_non_empty ) {
|
|
return;
|
|
}
|
|
|
|
$first_non_empty_content = TextStrings::stripQuotes( $this->tokens[ $first_non_empty ]['content'] );
|
|
|
|
// Try again with just the first token if it's a text string.
|
|
if ( isset( Tokens::$stringTokens[ $this->tokens[ $first_non_empty ]['code'] ] )
|
|
&& $this->is_prefixed( $target_param['start'], $first_non_empty_content ) === true
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $first_non_empty ]['code'] ) {
|
|
// If the first part of the parameter is a double quoted string, try again with only
|
|
// the part before the first variable (if any).
|
|
$exploded = explode( '$', $first_non_empty_content );
|
|
$first = rtrim( $exploded[0], '{' );
|
|
if ( '' !== $first ) {
|
|
if ( $this->is_prefixed( $target_param['start'], $first ) === true ) {
|
|
return;
|
|
}
|
|
} else {
|
|
// Start of hook/constant name is dynamic, throw a warning.
|
|
$is_error = false;
|
|
}
|
|
} elseif ( ! isset( Tokens::$stringTokens[ $this->tokens[ $first_non_empty ]['code'] ] ) ) {
|
|
// Dynamic hook/constant name, throw a warning.
|
|
$is_error = false;
|
|
}
|
|
}
|
|
|
|
if ( 'define' === $matched_content ) {
|
|
if ( \defined( '\\' . $clean_content ) ) {
|
|
// Backfill for PHP native constant.
|
|
return;
|
|
}
|
|
|
|
if ( strpos( $clean_content, '\\' ) !== false ) {
|
|
// Namespaced or unreachable constant.
|
|
return;
|
|
}
|
|
|
|
$data = array( 'Global constants defined' );
|
|
$error_code = 'NonPrefixedConstantFound';
|
|
if ( false === $is_error ) {
|
|
$error_code = 'VariableConstantNameFound';
|
|
}
|
|
} else {
|
|
$data = array( 'Hook names invoked' );
|
|
$error_code = 'NonPrefixedHooknameFound';
|
|
if ( false === $is_error ) {
|
|
$error_code = 'DynamicHooknameFound';
|
|
}
|
|
}
|
|
|
|
$data[] = $clean_content;
|
|
|
|
$recorded = MessageHelper::addMessage( $this->phpcsFile, self::ERROR_MSG, $first_non_empty, $is_error, $error_code, $data );
|
|
|
|
if ( true === $recorded ) {
|
|
$this->record_potential_prefix_metric( $stackPtr, $clean_content );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a function/class/constant/variable name is prefixed with one of the expected prefixes.
|
|
*
|
|
* @since 0.12.0
|
|
* @since 0.14.0 Allows for other non-word characters as well as underscores to better support hook names.
|
|
* @since 1.0.0 Does not require a word seperator anymore after a prefix.
|
|
* This allows for improved code style independent checking,
|
|
* i.e. allows for camelCase naming and the likes.
|
|
* @since 1.0.1 - Added $stackPtr parameter.
|
|
* - The function now also records metrics about the prefixes encountered.
|
|
*
|
|
* @param int $stackPtr The position of the token to record the metric against.
|
|
* @param string $name Name to check for a prefix.
|
|
*
|
|
* @return bool True when the name is one of the prefixes or starts with an allowed prefix.
|
|
* False otherwise.
|
|
*/
|
|
private function is_prefixed( $stackPtr, $name ) {
|
|
foreach ( $this->validated_prefixes as $prefix ) {
|
|
if ( stripos( $name, $prefix ) === 0 ) {
|
|
$this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $prefix );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a variable name might need a prefix.
|
|
*
|
|
* Prefix is not needed for:
|
|
* - superglobals,
|
|
* - WP native globals,
|
|
* - variables which are already prefixed.
|
|
*
|
|
* @since 0.12.0
|
|
* @since 1.0.1 Added $stackPtr parameter.
|
|
* @since 3.0.0 Renamed from `variable_prefixed_or_whitelisted()` to `variable_prefixed_or_allowed()`.
|
|
*
|
|
* @param int $stackPtr The position of the token to record the metric against.
|
|
* @param string $name Variable name without the dollar sign.
|
|
*
|
|
* @return bool True if the variable name is allowed or already prefixed.
|
|
* False otherwise.
|
|
*/
|
|
private function variable_prefixed_or_allowed( $stackPtr, $name ) {
|
|
// Ignore superglobals and WP global variables.
|
|
if ( Variables::isSuperglobalName( $name ) || WPGlobalVariablesHelper::is_wp_global( $name ) ) {
|
|
return true;
|
|
}
|
|
|
|
return $this->is_prefixed( $stackPtr, $name );
|
|
}
|
|
|
|
/**
|
|
* Validate an array of prefixes as passed through a custom property or via the command line.
|
|
*
|
|
* Checks that the prefix:
|
|
* - is not one of the blocked ones.
|
|
* - complies with the PHP rules for valid function, class, variable, constant names.
|
|
*
|
|
* @since 0.12.0
|
|
*
|
|
* @return void
|
|
*/
|
|
private function validate_prefixes() {
|
|
if ( $this->previous_prefixes === $this->prefixes ) {
|
|
return;
|
|
}
|
|
|
|
// Set the cache *before* validation so as to not break the above compare.
|
|
$this->previous_prefixes = $this->prefixes;
|
|
|
|
// Validate the passed prefix(es).
|
|
$prefixes = array();
|
|
$ns_prefixes = array();
|
|
foreach ( $this->prefixes as $key => $prefix ) {
|
|
$prefixLC = strtolower( $prefix );
|
|
|
|
if ( isset( $this->prefix_blocklist[ $prefixLC ] ) ) {
|
|
$this->phpcsFile->addError(
|
|
'The "%s" prefix is not allowed.',
|
|
0,
|
|
'ForbiddenPrefixPassed',
|
|
array( $prefix )
|
|
);
|
|
continue;
|
|
}
|
|
|
|
$prefix_length = strlen( $prefix );
|
|
if ( function_exists( 'iconv_strlen' ) ) {
|
|
$prefix_length = iconv_strlen( $prefix, Helper::getEncoding( $this->phpcsFile ) );
|
|
}
|
|
|
|
if ( $prefix_length < self::MIN_PREFIX_LENGTH ) {
|
|
$this->phpcsFile->addError(
|
|
'The "%s" prefix is too short. Short prefixes are not unique enough and may cause name collisions with other code.',
|
|
0,
|
|
'ShortPrefixPassed',
|
|
array( $prefix )
|
|
);
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Validate the prefix against characters allowed for function, class, constant names etc.
|
|
* Note: this does not use the PHPCSUtils `NamingConventions::isValidIdentifierName()` method
|
|
* as we want to allow namespace separators in the prefixes.
|
|
*/
|
|
if ( preg_match( '`^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\\\\]*$`', $prefix ) !== 1 ) {
|
|
|
|
$this->phpcsFile->addWarning(
|
|
'The "%s" prefix is not a valid namespace/function/class/variable/constant prefix in PHP.',
|
|
0,
|
|
'InvalidPrefixPassed',
|
|
array( $prefix )
|
|
);
|
|
}
|
|
|
|
// Lowercase the prefix to allow for direct compare.
|
|
$prefixes[ $key ] = $prefixLC;
|
|
|
|
/*
|
|
* Replace non-word characters in the prefix with a regex snippet, but only if the
|
|
* string doesn't already contain namespace separators.
|
|
*/
|
|
$is_regex = false;
|
|
if ( strpos( $prefix, '\\' ) === false && preg_match( '`[_\W]`', $prefix ) > 0 ) {
|
|
$prefix = preg_replace( '`([_\W])`', '[\\\\\\\\$1]', $prefixLC );
|
|
$is_regex = true;
|
|
}
|
|
|
|
$ns_prefixes[ $prefixLC ] = array(
|
|
'prefix' => $prefix,
|
|
'is_regex' => $is_regex,
|
|
);
|
|
}
|
|
|
|
// Set the validated prefixes caches.
|
|
$this->validated_prefixes = $prefixes;
|
|
$this->validated_namespace_prefixes = $ns_prefixes;
|
|
}
|
|
|
|
/**
|
|
* Record the "potential prefix" metric.
|
|
*
|
|
* @since 1.0.1
|
|
*
|
|
* @param int $stackPtr The position of the token to record the metric against.
|
|
* @param string $construct_name Name of the global construct to try and distill a potential prefix from.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function record_potential_prefix_metric( $stackPtr, $construct_name ) {
|
|
if ( preg_match( '`^([A-Z]*[a-z0-9]*+)`', ltrim( $construct_name, '\$_' ), $matches ) > 0
|
|
&& isset( $matches[1] ) && '' !== $matches[1]
|
|
) {
|
|
$this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: potential prefixes - start of non-prefixed construct', strtolower( $matches[1] ) );
|
|
}
|
|
}
|
|
}
|