<?php
/**
 * Represents a PHP_CodeSniffer sniff for sniffing WordPress coding standards.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPress;

use PHP_CodeSniffer_Sniff as PHPCS_Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;
use WordPress\PHPCSHelper;

/**
 * Represents a PHP_CodeSniffer sniff for sniffing WordPress coding standards.
 *
 * Provides a bootstrap for the sniffs, to reduce code duplication.
 *
 * @package WPCS\WordPressCodingStandards
 * @since   0.4.0
 *
 * {@internal This class contains numerous properties where the array format looks
 *            like `'string' => true`, i.e. the array item is set as the array key.
 *            This allows for sniffs to verify whether something is in one of these
 *            lists using `isset()` rather than `in_array()` which is a much more
 *            efficient (faster) check to execute and therefore improves the
 *            performance of the sniffs.
 *            The `true` value in those cases is used as a placeholder and has no
 *            meaning in and of itself.
 *            In the rare few cases where the array values *do* have meaning, this
 *            is documented in the property documentation.}}
 */
abstract class Sniff implements PHPCS_Sniff {

	/**
	 * Regex to get complex variables from T_DOUBLE_QUOTED_STRING or T_HEREDOC.
	 *
	 * @since 0.14.0
	 *
	 * @var string
	 */
	const REGEX_COMPLEX_VARS = '`(?:(\{)?(?<!\\\\)\$)?(\{)?(?<!\\\\)\$(\{)?(?P<varname>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:->\$?(?P>varname)|\[[^\]]+\]|::\$?(?P>varname)|\([^\)]*\))*(?(3)\}|)(?(2)\}|)(?(1)\}|)`';

	/**
	 * Minimum supported WordPress version.
	 *
	 * Currently used by the `WordPress.WP.AlternativeFunctions`,
	 * `WordPress.WP.DeprecatedClasses`, `WordPress.WP.DeprecatedFunctions`
	 * and the `WordPress.WP.DeprecatedParameter` sniff.
	 *
	 * These sniffs will throw an error when usage of a deprecated class/function/parameter
	 * is detected if the class/function/parameter was deprecated before the minimum
	 * supported WP version; a warning otherwise.
	 * By default, it is set to presume that a project will support the current
	 * WP version and up to three releases before.
	 *
	 * This property allows changing the minimum supported WP version used by
	 * these sniffs by setting a property in a custom phpcs.xml ruleset.
	 * This property will need to be set for each sniff which uses it.
	 *
	 * Example usage:
	 * <rule ref="WordPress.WP.DeprecatedClasses">
	 *  <properties>
	 *   <property name="minimum_supported_version" value="4.3"/>
	 *  </properties>
	 * </rule>
	 *
	 * Alternatively, the value can be passed in one go for all sniff using it via
	 * the command line or by setting a `<config>` value in a custom phpcs.xml ruleset.
	 * Note: the `_wp_` in the command line property name!
	 *
	 * CL: `phpcs --runtime-set minimum_supported_wp_version 4.5`
	 * Ruleset: `<config name="minimum_supported_wp_version" value="4.5"/>`
	 *
	 * @since 0.14.0 Previously the individual sniffs each contained this property.
	 *
	 * @internal When the value of this property is changed, it will also need
	 *           to be changed in the `WP/AlternativeFunctionsUnitTest.inc` file.
	 *
	 * @var string WordPress version.
	 */
	public $minimum_supported_version = '4.7';

	/**
	 * Custom list of classes which test classes can extend.
	 *
	 * This property allows end-users to add to the $test_class_whitelist via their ruleset.
	 * This property will need to be set for each sniff which uses the
	 * `is_test_class()` method.
	 * Currently the method is used by the `WordPress.WP.GlobalVariablesOverride`,
	 * `WordPress.NamingConventions.PrefixAllGlobals` and the `WordPress.Files.Filename` sniffs.
	 *
	 * Example usage:
	 * <rule ref="WordPress.[Subset].[Sniffname]">
	 *  <properties>
	 *   <property name="custom_test_class_whitelist" type="array" value="My_Plugin_First_Test_Class,My_Plugin_Second_Test_Class"/>
	 *  </properties>
	 * </rule>
	 *
	 * @since 0.11.0
	 *
	 * @var string|string[]
	 */
	public $custom_test_class_whitelist = array();

	/**
	 * List of the functions which verify nonces.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $nonceVerificationFunctions = array(
		'wp_verify_nonce'     => true,
		'check_admin_referer' => true,
		'check_ajax_referer'  => true,
	);

	/**
	 * Functions that escape values for display.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $escapingFunctions = array(
		'absint'               => true,
		'esc_attr__'           => true,
		'esc_attr_e'           => true,
		'esc_attr_x'           => true,
		'esc_attr'             => true,
		'esc_html__'           => true,
		'esc_html_e'           => true,
		'esc_html_x'           => true,
		'esc_html'             => true,
		'esc_js'               => true,
		'esc_sql'              => true,
		'esc_textarea'         => true,
		'esc_url_raw'          => true,
		'esc_url'              => true,
		'filter_input'         => true,
		'filter_var'           => true,
		'floatval'             => true,
		'intval'               => true,
		'json_encode'          => true,
		'like_escape'          => true,
		'number_format'        => true,
		'rawurlencode'         => true,
		'sanitize_html_class'  => true,
		'sanitize_user_field'  => true,
		'tag_escape'           => true,
		'urlencode_deep'       => true,
		'urlencode'            => true,
		'wp_json_encode'       => true,
		'wp_kses_allowed_html' => true,
		'wp_kses_data'         => true,
		'wp_kses_post'         => true,
		'wp_kses'              => true,
	);

	/**
	 * Functions whose output is automatically escaped for display.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $autoEscapedFunctions = array(
		'allowed_tags'              => true,
		'bloginfo'                  => true,
		'body_class'                => true,
		'calendar_week_mod'         => true,
		'category_description'      => true,
		'checked'                   => true,
		'comment_class'             => true,
		'count'                     => true,
		'disabled'                  => true,
		'do_shortcode'              => true,
		'do_shortcode_tag'          => true,
		'get_archives_link'         => true,
		'get_attachment_link'       => true,
		'get_avatar'                => true,
		'get_bookmark_field'        => true,
		'get_calendar'              => true,
		'get_comment_author_link'   => true,
		'get_current_blog_id'       => true,
		'get_delete_post_link'      => true,
		'get_search_form'           => true,
		'get_search_query'          => true,
		'get_the_author_link'       => true,
		'get_the_author'            => true,
		'get_the_date'              => true,
		'get_the_ID'                => true,
		'get_the_post_thumbnail'    => true,
		'get_the_term_list'         => true,
		'paginate_comments_links'   => true,
		'post_type_archive_title'   => true,
		'readonly'                  => true,
		'selected'                  => true,
		'single_cat_title'          => true,
		'single_month_title'        => true,
		'single_post_title'         => true,
		'single_tag_title'          => true,
		'single_term_title'         => true,
		'tag_description'           => true,
		'term_description'          => true,
		'the_author'                => true,
		'the_date'                  => true,
		'the_title_attribute'       => true,
		'vip_powered_wpcom'         => true,
		'walk_nav_menu_tree'        => true,
		'wp_dropdown_categories'    => true,
		'wp_dropdown_users'         => true,
		'wp_generate_tag_cloud'     => true,
		'wp_get_archives'           => true,
		'wp_get_attachment_image'   => true,
		'wp_get_attachment_link'    => true,
		'wp_link_pages'             => true,
		'wp_list_authors'           => true,
		'wp_list_bookmarks'         => true,
		'wp_list_categories'        => true,
		'wp_list_comments'          => true,
		'wp_login_form'             => true,
		'wp_loginout'               => true,
		'wp_nav_menu'               => true,
		'wp_register'               => true,
		'wp_tag_cloud'              => true,
		'wp_title'                  => true,
	);

	/**
	 * Functions that sanitize values.
	 *
	 * This list is complementary to the `$unslashingSanitizingFunctions`
	 * list.
	 * Sanitizing functions should be added to this list if they do *not*
	 * implicitely unslash data and to the `$unslashingsanitizingFunctions`
	 * list if they do.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $sanitizingFunctions = array(
		'_wp_handle_upload'          => true,
		'array_key_exists'           => true,
		'esc_url_raw'                => true,
		'filter_input'               => true,
		'filter_var'                 => true,
		'hash_equals'                => true,
		'in_array'                   => true,
		'is_email'                   => true,
		'number_format'              => true,
		'sanitize_bookmark_field'    => true,
		'sanitize_bookmark'          => true,
		'sanitize_email'             => true,
		'sanitize_file_name'         => true,
		'sanitize_hex_color_no_hash' => true,
		'sanitize_hex_color'         => true,
		'sanitize_html_class'        => true,
		'sanitize_meta'              => true,
		'sanitize_mime_type'         => true,
		'sanitize_option'            => true,
		'sanitize_sql_orderby'       => true,
		'sanitize_term_field'        => true,
		'sanitize_term'              => true,
		'sanitize_text_field'        => true,
		'sanitize_textarea_field'    => true,
		'sanitize_title_for_query'   => true,
		'sanitize_title_with_dashes' => true,
		'sanitize_title'             => true,
		'sanitize_user_field'        => true,
		'sanitize_user'              => true,
		'validate_file'              => true,
		'wp_handle_sideload'         => true,
		'wp_handle_upload'           => true,
		'wp_kses_allowed_html'       => true,
		'wp_kses_data'               => true,
		'wp_kses_post'               => true,
		'wp_kses'                    => true,
		'wp_parse_id_list'           => true,
		'wp_redirect'                => true,
		'wp_safe_redirect'           => true,
		'wp_strip_all_tags'          => true,
	);

	/**
	 * Sanitizing functions that implicitly unslash the data passed to them.
	 *
	 * This list is complementary to the `$sanitizingFunctions` list.
	 * Sanitizing functions should be added to this list if they also
	 * implicitely unslash data and to the `$sanitizingFunctions` list
	 * if they don't.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $unslashingSanitizingFunctions = array(
		'absint'       => true,
		'boolval'      => true,
		'floatval'     => true,
		'intval'       => true,
		'is_array'     => true,
		'sanitize_key' => true,
	);

	/**
	 * Token which when they preceed code indicate the value is safely casted.
	 *
	 * @since 1.1.0
	 *
	 * @var array
	 */
	protected $safe_casts = array(
		\T_INT_CAST    => true,
		\T_DOUBLE_CAST => true,
		\T_BOOL_CAST   => true,
	);

	/**
	 * Functions that format strings.
	 *
	 * These functions are often used for formatting values just before output, and
	 * it is common practice to escape the individual parameters passed to them as
	 * needed instead of escaping the entire result. This is especially true when the
	 * string being formatted contains HTML, which makes escaping the full result
	 * more difficult.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $formattingFunctions = array(
		'array_fill' => true,
		'ent2ncr'    => true,
		'implode'    => true,
		'join'       => true,
		'nl2br'      => true,
		'sprintf'    => true,
		'vsprintf'   => true,
		'wp_sprintf' => true,
	);

	/**
	 * Functions which print output incorporating the values passed to them.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $printingFunctions = array(
		'_deprecated_argument'    => true,
		'_deprecated_constructor' => true,
		'_deprecated_file'        => true,
		'_deprecated_function'    => true,
		'_deprecated_hook'        => true,
		'_doing_it_wrong'         => true,
		'_e'                      => true,
		'_ex'                     => true,
		'printf'                  => true,
		'trigger_error'           => true,
		'user_error'              => true,
		'vprintf'                 => true,
		'wp_die'                  => true,
		'wp_dropdown_pages'       => true,
	);

	/**
	 * Functions that escape values for use in SQL queries.
	 *
	 * @since 0.9.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $SQLEscapingFunctions = array(
		'absint'      => true,
		'esc_sql'     => true,
		'floatval'    => true,
		'intval'      => true,
		'like_escape' => true,
	);

	/**
	 * Functions whose output is automatically escaped for use in SQL queries.
	 *
	 * @since 0.9.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $SQLAutoEscapedFunctions = array(
		'count' => true,
	);

	/**
	 * A list of functions that get data from the cache.
	 *
	 * @since 0.6.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $cacheGetFunctions = array(
		'wp_cache_get' => true,
	);

	/**
	 * A list of functions that set data in the cache.
	 *
	 * @since 0.6.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $cacheSetFunctions = array(
		'wp_cache_set' => true,
		'wp_cache_add' => true,
	);

	/**
	 * A list of functions that delete data from the cache.
	 *
	 * @since 0.6.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $cacheDeleteFunctions = array(
		'wp_cache_delete'         => true,
		'clean_attachment_cache'  => true,
		'clean_blog_cache'        => true,
		'clean_bookmark_cache'    => true,
		'clean_category_cache'    => true,
		'clean_comment_cache'     => true,
		'clean_network_cache'     => true,
		'clean_object_term_cache' => true,
		'clean_page_cache'        => true,
		'clean_post_cache'        => true,
		'clean_term_cache'        => true,
		'clean_user_cache'        => true,
	);

	/**
	 * A list of functions that invoke WP hooks (filters/actions).
	 *
	 * @since 0.10.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $hookInvokeFunctions = array(
		'do_action'                => true,
		'do_action_ref_array'      => true,
		'do_action_deprecated'     => true,
		'apply_filters'            => true,
		'apply_filters_ref_array'  => true,
		'apply_filters_deprecated' => true,
	);

	/**
	 * A list of functions that are used to interact with the WP plugins API.
	 *
	 * @since 0.10.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array <string function name> => <int position of the hook name argument in function signature>
	 */
	protected $hookFunctions = array(
		'has_filter'         => 1,
		'add_filter'         => 1,
		'remove_filter'      => 1,
		'remove_all_filters' => 1,
		'doing_filter'       => 1, // Hook name optional.
		'has_action'         => 1,
		'add_action'         => 1,
		'doing_action'       => 1, // Hook name optional.
		'did_action'         => 1,
		'remove_action'      => 1,
		'remove_all_actions' => 1,
		'current_filter'     => 0, // No hook name argument.
	);

	/**
	 * List of global WP variables.
	 *
	 * @since 0.3.0
	 * @since 0.11.0 Changed visibility from public to protected.
	 * @since 0.12.0 Renamed from `$globals` to `$wp_globals` to be more descriptive.
	 * @since 0.12.0 Moved from WordPress_Sniffs_Variables_GlobalVariablesSniff to WordPress_Sniff
	 *
	 * @var array
	 */
	protected $wp_globals = array(
		'_links_add_base'                  => true,
		'_links_add_target'                => true,
		'_menu_item_sort_prop'             => true,
		'_nav_menu_placeholder'            => true,
		'_new_bundled_files'               => true,
		'_old_files'                       => true,
		'_parent_pages'                    => true,
		'_registered_pages'                => true,
		'_updated_user_settings'           => true,
		'_wp_additional_image_sizes'       => true,
		'_wp_admin_css_colors'             => true,
		'_wp_default_headers'              => true,
		'_wp_deprecated_widgets_callbacks' => true,
		'_wp_last_object_menu'             => true,
		'_wp_last_utility_menu'            => true,
		'_wp_menu_nopriv'                  => true,
		'_wp_nav_menu_max_depth'           => true,
		'_wp_post_type_features'           => true,
		'_wp_real_parent_file'             => true,
		'_wp_registered_nav_menus'         => true,
		'_wp_sidebars_widgets'             => true,
		'_wp_submenu_nopriv'               => true,
		'_wp_suspend_cache_invalidation'   => true,
		'_wp_theme_features'               => true,
		'_wp_using_ext_object_cache'       => true,
		'action'                           => true,
		'active_signup'                    => true,
		'admin_body_class'                 => true,
		'admin_page_hooks'                 => true,
		'all_links'                        => true,
		'allowedentitynames'               => true,
		'allowedposttags'                  => true,
		'allowedtags'                      => true,
		'auth_secure_cookie'               => true,
		'authordata'                       => true,
		'avail_post_mime_types'            => true,
		'avail_post_stati'                 => true,
		'blog_id'                          => true,
		'blog_title'                       => true,
		'blogname'                         => true,
		'cat'                              => true,
		'cat_id'                           => true,
		'charset_collate'                  => true,
		'comment'                          => true,
		'comment_alt'                      => true,
		'comment_depth'                    => true,
		'comment_status'                   => true,
		'comment_thread_alt'               => true,
		'comment_type'                     => true,
		'comments'                         => true,
		'compress_css'                     => true,
		'compress_scripts'                 => true,
		'concatenate_scripts'              => true,
		'current_screen'                   => true,
		'current_site'                     => true,
		'current_user'                     => true,
		'currentcat'                       => true,
		'currentday'                       => true,
		'currentmonth'                     => true,
		'custom_background'                => true,
		'custom_image_header'              => true,
		'default_menu_order'               => true,
		'descriptions'                     => true,
		'domain'                           => true,
		'editor_styles'                    => true,
		'error'                            => true,
		'errors'                           => true,
		'EZSQL_ERROR'                      => true,
		'feeds'                            => true,
		'GETID3_ERRORARRAY'                => true,
		'hook_suffix'                      => true,
		'HTTP_RAW_POST_DATA'               => true,
		'id'                               => true,
		'in_comment_loop'                  => true,
		'interim_login'                    => true,
		'is_apache'                        => true,
		'is_chrome'                        => true,
		'is_gecko'                         => true,
		'is_IE'                            => true,
		'is_IIS'                           => true,
		'is_iis7'                          => true,
		'is_macIE'                         => true,
		'is_NS4'                           => true,
		'is_opera'                         => true,
		'is_safari'                        => true,
		'is_winIE'                         => true,
		'l10n'                             => true,
		'link'                             => true,
		'link_id'                          => true,
		'locale'                           => true,
		'locked_post_status'               => true,
		'lost'                             => true,
		'm'                                => true,
		'map'                              => true,
		'menu'                             => true,
		'menu_order'                       => true,
		'merged_filters'                   => true,
		'mode'                             => true,
		'monthnum'                         => true,
		'more'                             => true,
		'multipage'                        => true,
		'names'                            => true,
		'nav_menu_selected_id'             => true,
		'new_whitelist_options'            => true,
		'numpages'                         => true,
		'one_theme_location_no_menus'      => true,
		'opml'                             => true,
		'order'                            => true,
		'orderby'                          => true,
		'overridden_cpage'                 => true,
		'page'                             => true,
		'paged'                            => true,
		'pagenow'                          => true,
		'pages'                            => true,
		'parent_file'                      => true,
		'pass_allowed_html'                => true,
		'pass_allowed_protocols'           => true,
		'path'                             => true,
		'per_page'                         => true,
		'PHP_SELF'                         => true,
		'phpmailer'                        => true,
		'plugin_page'                      => true,
		'plugins'                          => true,
		'post'                             => true,
		'post_default_category'            => true,
		'post_default_title'               => true,
		'post_ID'                          => true,
		'post_id'                          => true,
		'post_mime_types'                  => true,
		'post_type'                        => true,
		'post_type_object'                 => true,
		'posts'                            => true,
		'preview'                          => true,
		'previouscat'                      => true,
		'previousday'                      => true,
		'previousweekday'                  => true,
		'redir_tab'                        => true,
		'required_mysql_version'           => true,
		'required_php_version'             => true,
		'rnd_value'                        => true,
		'role'                             => true,
		's'                                => true,
		'search'                           => true,
		'self'                             => true,
		'shortcode_tags'                   => true,
		'show_admin_bar'                   => true,
		'sidebars_widgets'                 => true,
		'status'                           => true,
		'submenu'                          => true,
		'submenu_file'                     => true,
		'super_admins'                     => true,
		'tab'                              => true,
		'table_prefix'                     => true,
		'tabs'                             => true,
		'tag'                              => true,
		'targets'                          => true,
		'tax'                              => true,
		'taxnow'                           => true,
		'taxonomy'                         => true,
		'term'                             => true,
		'text_direction'                   => true,
		'theme_field_defaults'             => true,
		'themes_allowedtags'               => true,
		'timeend'                          => true,
		'timestart'                        => true,
		'tinymce_version'                  => true,
		'title'                            => true,
		'totals'                           => true,
		'type'                             => true,
		'typenow'                          => true,
		'updated_timestamp'                => true,
		'upgrading'                        => true,
		'urls'                             => true,
		'user_email'                       => true,
		'user_ID'                          => true,
		'user_identity'                    => true,
		'user_level'                       => true,
		'user_login'                       => true,
		'user_url'                         => true,
		'userdata'                         => true,
		'usersearch'                       => true,
		'whitelist_options'                => true,
		'withcomments'                     => true,
		'wp'                               => true,
		'wp_actions'                       => true,
		'wp_admin_bar'                     => true,
		'wp_cockneyreplace'                => true,
		'wp_current_db_version'            => true,
		'wp_current_filter'                => true,
		'wp_customize'                     => true,
		'wp_dashboard_control_callbacks'   => true,
		'wp_db_version'                    => true,
		'wp_did_header'                    => true,
		'wp_embed'                         => true,
		'wp_file_descriptions'             => true,
		'wp_filesystem'                    => true,
		'wp_filter'                        => true,
		'wp_hasher'                        => true,
		'wp_header_to_desc'                => true,
		'wp_importers'                     => true,
		'wp_json'                          => true,
		'wp_list_table'                    => true,
		'wp_local_package'                 => true,
		'wp_locale'                        => true,
		'wp_meta_boxes'                    => true,
		'wp_object_cache'                  => true,
		'wp_plugin_paths'                  => true,
		'wp_post_statuses'                 => true,
		'wp_post_types'                    => true,
		'wp_queries'                       => true,
		'wp_query'                         => true,
		'wp_registered_sidebars'           => true,
		'wp_registered_widget_controls'    => true,
		'wp_registered_widget_updates'     => true,
		'wp_registered_widgets'            => true,
		'wp_rewrite'                       => true,
		'wp_rich_edit'                     => true,
		'wp_rich_edit_exists'              => true,
		'wp_roles'                         => true,
		'wp_scripts'                       => true,
		'wp_settings_errors'               => true,
		'wp_settings_fields'               => true,
		'wp_settings_sections'             => true,
		'wp_smiliessearch'                 => true,
		'wp_styles'                        => true,
		'wp_taxonomies'                    => true,
		'wp_the_query'                     => true,
		'wp_theme_directories'             => true,
		'wp_themes'                        => true,
		'wp_user_roles'                    => true,
		'wp_version'                       => true,
		'wp_widget_factory'                => true,
		'wp_xmlrpc_server'                 => true,
		'wpcommentsjavascript'             => true,
		'wpcommentspopupfile'              => true,
		'wpdb'                             => true,
		'wpsmiliestrans'                   => true,
		'year'                             => true,
	);

	/**
	 * A list of superglobals that incorporate user input.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 Changed from static to non-static.
	 *
	 * @var string[]
	 */
	protected $input_superglobals = array(
		'$_COOKIE',
		'$_GET',
		'$_FILES',
		'$_POST',
		'$_REQUEST',
		'$_SERVER',
	);

	/**
	 * Whitelist of classes which test classes can extend.
	 *
	 * @since 0.11.0
	 *
	 * @var string[]
	 */
	protected $test_class_whitelist = array(
		'WP_UnitTestCase'                            => true,
		'WP_Ajax_UnitTestCase'                       => true,
		'WP_Canonical_UnitTestCase'                  => true,
		'WP_Test_REST_TestCase'                      => true,
		'WP_Test_REST_Controller_Testcase'           => true,
		'WP_Test_REST_Post_Type_Controller_Testcase' => true,
		'WP_XMLRPC_UnitTestCase'                     => true,
		'PHPUnit_Framework_TestCase'                 => true,
		'PHPUnit\Framework\TestCase'                 => true,
	);

	/**
	 * The token "type" values for the PHPCS 3.2+ whitelist comments.
	 *
	 * PHPCS cross-version compatibility layer to allow sniffs to
	 * allow for the new PHPCS annotation comments without breaking in older
	 * PHPCS versions.
	 *
	 * @internal Can be replaced with using the PHPCS native Token::$phpcsCommentTokens
	 *           array once the minimum WPCS requirement for PHPCS has gone up
	 *           to PHPCS 3.2.3.
	 *           Note: The PHPCS native property uses the constants/ token "code",
	 *           so code referring to this property will need to be adjusted when
	 *           the property is removed.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $phpcsCommentTokens = array(
		'T_PHPCS_ENABLE'      => true,
		'T_PHPCS_DISABLE'     => true,
		'T_PHPCS_SET'         => true,
		'T_PHPCS_IGNORE'      => true,
		'T_PHPCS_IGNORE_FILE' => true,
	);

	/**
	 * The current file being sniffed.
	 *
	 * @since 0.4.0
	 *
	 * @var \PHP_CodeSniffer\Files\File
	 */
	protected $phpcsFile;

	/**
	 * The list of tokens in the current file being sniffed.
	 *
	 * @since 0.4.0
	 *
	 * @var array
	 */
	protected $tokens;

	/**
	 * Set sniff properties and hand off to child class for processing of the token.
	 *
	 * @since 0.11.0
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
	 * @param int                         $stackPtr  The position of the current token
	 *                                               in the stack passed in $tokens.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	public function process( File $phpcsFile, $stackPtr ) {
		$this->init( $phpcsFile );
		return $this->process_token( $stackPtr );
	}

	/**
	 * Processes a sniff when one of its tokens is encountered.
	 *
	 * @since 0.11.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.
	 */
	abstract public function process_token( $stackPtr );

	/**
	 * Initialize the class for the current process.
	 *
	 * This method must be called by child classes before using many of the methods
	 * below.
	 *
	 * @since 0.4.0
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file currently being processed.
	 */
	protected function init( File $phpcsFile ) {
		$this->phpcsFile = $phpcsFile;
		$this->tokens    = $phpcsFile->getTokens();
	}

	/**
	 * Strip quotes surrounding an arbitrary string.
	 *
	 * Intended for use with the contents of a T_CONSTANT_ENCAPSED_STRING / T_DOUBLE_QUOTED_STRING.
	 *
	 * @since 0.11.0
	 *
	 * @param string $string The raw string.
	 * @return string String without quotes around it.
	 */
	public function strip_quotes( $string ) {
		return preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $string );
	}

	/**
	 * Add a PHPCS message to the output stack as either a warning or an error.
	 *
	 * @since 0.11.0
	 *
	 * @param string $message   The message.
	 * @param int    $stackPtr  The position of the token the message relates to.
	 * @param bool   $is_error  Optional. Whether to report the message as an 'error' or 'warning'.
	 *                          Defaults to true (error).
	 * @param string $code      Optional error code for the message. Defaults to 'Found'.
	 * @param array  $data      Optional input for the data replacements.
	 * @param int    $severity  Optional. Severity level. Defaults to 0 which will translate to
	 *                          the PHPCS default severity level.
	 * @return bool
	 */
	protected function addMessage( $message, $stackPtr, $is_error = true, $code = 'Found', $data = array(), $severity = 0 ) {
		return $this->throwMessage( $message, $stackPtr, $is_error, $code, $data, $severity, false );
	}

	/**
	 * Add a fixable PHPCS message to the output stack as either a warning or an error.
	 *
	 * @since 0.11.0
	 *
	 * @param string $message   The message.
	 * @param int    $stackPtr  The position of the token the message relates to.
	 * @param bool   $is_error  Optional. Whether to report the message as an 'error' or 'warning'.
	 *                          Defaults to true (error).
	 * @param string $code      Optional error code for the message. Defaults to 'Found'.
	 * @param array  $data      Optional input for the data replacements.
	 * @param int    $severity  Optional. Severity level. Defaults to 0 which will translate to
	 *                          the PHPCS default severity level.
	 * @return bool
	 */
	protected function addFixableMessage( $message, $stackPtr, $is_error = true, $code = 'Found', $data = array(), $severity = 0 ) {
		return $this->throwMessage( $message, $stackPtr, $is_error, $code, $data, $severity, true );
	}

	/**
	 * Add a PHPCS message to the output stack as either a warning or an error.
	 *
	 * @since 0.11.0
	 *
	 * @param string $message   The message.
	 * @param int    $stackPtr  The position of the token the message relates to.
	 * @param bool   $is_error  Optional. Whether to report the message as an 'error' or 'warning'.
	 *                          Defaults to true (error).
	 * @param string $code      Optional error code for the message. Defaults to 'Found'.
	 * @param array  $data      Optional input for the data replacements.
	 * @param int    $severity  Optional. Severity level. Defaults to 0 which will translate to
	 *                          the PHPCS default severity level.
	 * @param bool   $fixable   Optional. Whether this is a fixable error. Defaults to false.
	 * @return bool
	 */
	private function throwMessage( $message, $stackPtr, $is_error = true, $code = 'Found', $data = array(), $severity = 0, $fixable = false ) {

		$method = 'add';
		if ( true === $fixable ) {
			$method .= 'Fixable';
		}

		if ( true === $is_error ) {
			$method .= 'Error';
		} else {
			$method .= 'Warning';
		}

		return \call_user_func( array( $this->phpcsFile, $method ), $message, $stackPtr, $code, $data, $severity );
	}

	/**
	 * Convert an arbitrary string to an alphanumeric string with underscores.
	 *
	 * Pre-empt issues with arbitrary strings being used as error codes in XML and PHP.
	 *
	 * @since 0.11.0
	 *
	 * @param string $base_string Arbitrary string.
	 *
	 * @return string
	 */
	protected function string_to_errorcode( $base_string ) {
		return preg_replace( '`[^a-z0-9_]`i', '_', $base_string );
	}

	/**
	 * Merge a pre-set array with a ruleset provided array or inline provided string.
	 *
	 * - Will correctly handle custom array properties which were set without
	 *   the `type="array"` indicator.
	 *   This also allows for making these custom array properties testable using
	 *   a `@codingStandardsChangeSetting` comment in the unit tests.
	 * - By default flips custom lists to allow for using `isset()` instead
	 *   of `in_array()`.
	 * - When `$flip` is true:
	 *   * Presumes the base array is in a `'value' => true` format.
	 *   * Any custom items will be given the value `false` to be able to
	 *     distinguish them from pre-set (base array) values.
	 *   * Will filter previously added custom items out from the base array
	 *     before merging/returning to allow for resetting to the base array.
	 *
	 * {@internal Function is static as it doesn't use any of the properties or others
	 * methods anyway and this way the `WordPress_Sniffs_NamingConventions_ValidVariableNameSniff`
	 * which extends an upstream sniff can also use it.}}
	 *
	 * @since 0.11.0
	 *
	 * @param array|string $custom Custom list as provided via a ruleset.
	 *                             Can be either a comma-delimited string or
	 *                             an array of values.
	 * @param array        $base   Optional. Base list. Defaults to an empty array.
	 *                             Expects `value => true` format when `$flip` is true.
	 * @param bool         $flip   Optional. Whether or not to flip the custom list.
	 *                             Defaults to true.
	 * @return array
	 */
	public static function merge_custom_array( $custom, $base = array(), $flip = true ) {
		if ( true === $flip ) {
			$base = array_filter( $base );
		}

		if ( empty( $custom ) || ( ! \is_array( $custom ) && ! \is_string( $custom ) ) ) {
			return $base;
		}

		// Allow for a comma delimited list.
		if ( \is_string( $custom ) ) {
			$custom = explode( ',', $custom );
		}

		// Always trim whitespace from the values.
		$custom = array_filter( array_map( 'trim', $custom ) );

		if ( true === $flip ) {
			$custom = array_fill_keys( $custom, false );
		}

		if ( empty( $base ) ) {
			return $custom;
		}

		return array_merge( $base, $custom );
	}

	/**
	 * Get the last pointer in a line.
	 *
	 * @since 0.4.0
	 *
	 * @param integer $stackPtr The position of the current token in the stack passed
	 *                          in $tokens.
	 *
	 * @return integer Position of the last pointer on that line.
	 */
	protected function get_last_ptr_on_line( $stackPtr ) {

		$tokens      = $this->tokens;
		$currentLine = $tokens[ $stackPtr ]['line'];
		$nextPtr     = ( $stackPtr + 1 );

		while ( isset( $tokens[ $nextPtr ] ) && $tokens[ $nextPtr ]['line'] === $currentLine ) {
			$nextPtr++;
			// Do nothing, we just want the last token of the line.
		}

		// We've made it to the next line, back up one to the last in the previous line.
		// We do this for micro-optimization of the above loop.
		$lastPtr = ( $nextPtr - 1 );

		return $lastPtr;
	}

	/**
	 * Overrule the minimum supported WordPress version with a command-line/config value.
	 *
	 * Handle setting the minimum supported WP version in one go for all sniffs which
	 * expect it via the command line or via a `<config>` variable in a ruleset.
	 * The config variable overrules the default `$minimum_supported_version` and/or a
	 * `$minimum_supported_version` set for individual sniffs through the ruleset.
	 *
	 * @since 0.14.0
	 */
	protected function get_wp_version_from_cl() {
		$cl_supported_version = trim( PHPCSHelper::get_config_data( 'minimum_supported_wp_version' ) );
		if ( ! empty( $cl_supported_version )
			&& filter_var( $cl_supported_version, \FILTER_VALIDATE_FLOAT ) !== false
		) {
			$this->minimum_supported_version = $cl_supported_version;
		}
	}

	/**
	 * Find whitelisting comment.
	 *
	 * Comment must be at the end of the line or at the end of the statement
	 * and must use // format.
	 * It can be prefixed or suffixed with anything e.g. "foobar" will match:
	 * ... // foobar okay
	 * ... // WPCS: foobar whitelist.
	 *
	 * There is an exception, and that is when PHP is being interspersed with HTML.
	 * In that case, the comment should always come at the end of the statement (right
	 * before the closing tag, ?>). For example:
	 *
	 * <input type="text" id="<?php echo $id; // XSS OK ?>" />
	 *
	 * @since 0.4.0
	 * @since 0.14.0 Whitelist comments at the end of the statement are now also accepted.
	 *
	 * @param string  $comment  Comment to find.
	 * @param integer $stackPtr The position of the current token in the stack passed
	 *                          in $tokens.
	 *
	 * @return boolean True if whitelisting comment was found, false otherwise.
	 */
	protected function has_whitelist_comment( $comment, $stackPtr ) {

		// Respect the PHPCS 3.x --ignore-annotations setting.
		if ( true === PHPCSHelper::ignore_annotations( $this->phpcsFile ) ) {
			return false;
		}

		$regex = '#\b' . preg_quote( $comment, '#' ) . '\b#i';

		// There is a findEndOfStatement() method, but it considers more tokens than
		// we need to consider here.
		$end_of_statement = $this->phpcsFile->findNext( array( \T_CLOSE_TAG, \T_SEMICOLON ), $stackPtr );

		if ( false !== $end_of_statement ) {
			// If the statement was ended by a semicolon, check if there is a whitelist comment directly after it.
			if ( \T_SEMICOLON === $this->tokens[ $end_of_statement ]['code'] ) {
				$lastPtr = $this->phpcsFile->findNext( \T_WHITESPACE, ( $end_of_statement + 1 ), null, true );
			} elseif ( \T_CLOSE_TAG === $this->tokens[ $end_of_statement ]['code'] ) {
				// If the semicolon was left out and it was terminated by an ending tag, we need to look backwards.
				$lastPtr = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $end_of_statement - 1 ), null, true );
			}

			if ( ( ( \T_COMMENT === $this->tokens[ $lastPtr ]['code']
					&& strpos( $this->tokens[ $lastPtr ]['content'], '@codingStandardsChangeSetting' ) === false )
					|| ( isset( $this->phpcsCommentTokens[ $this->tokens[ $lastPtr ]['type'] ] )
					&& 'T_PHPCS_SET' !== $this->tokens[ $lastPtr ]['type'] ) )
				&& $this->tokens[ $lastPtr ]['line'] === $this->tokens[ $end_of_statement ]['line']
				&& preg_match( $regex, $this->tokens[ $lastPtr ]['content'] ) === 1
			) {
				return true;
			}
		}

		// No whitelist comment found so far. Check at the end of the stackPtr line.
		// Note: a T_COMMENT includes the new line character, so may be the last token on the line!
		$end_of_line = $this->get_last_ptr_on_line( $stackPtr );
		$lastPtr     = $this->phpcsFile->findPrevious( \T_WHITESPACE, $end_of_line, null, true );

		if ( ( ( \T_COMMENT === $this->tokens[ $lastPtr ]['code']
				&& strpos( $this->tokens[ $lastPtr ]['content'], '@codingStandardsChangeSetting' ) === false )
				|| ( isset( $this->phpcsCommentTokens[ $this->tokens[ $lastPtr ]['type'] ] )
				&& 'T_PHPCS_SET' !== $this->tokens[ $lastPtr ]['type'] ) )
			&& $this->tokens[ $lastPtr ]['line'] === $this->tokens[ $stackPtr ]['line']
			&& preg_match( $regex, $this->tokens[ $lastPtr ]['content'] ) === 1
		) {
			return true;
		}

		return false;
	}

	/**
	 * Check if a token is used within a unit test.
	 *
	 * Unit test methods are identified as such:
	 * - Method is within a known unit test class;
	 * - or Method is within a class/trait which extends a known unit test class.
	 *
	 * @since 0.11.0
	 * @since 1.1.0  Supports anonymous test classes and improved handling of nested scopes.
	 *
	 * @param int $stackPtr The position of the token to be examined.
	 *
	 * @return bool True if the token is within a unit test, false otherwise.
	 */
	protected function is_token_in_test_method( $stackPtr ) {
		// Is the token inside of a function definition ?
		$functionToken = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
		if ( false === $functionToken ) {
			// No conditions or no function condition.
			return false;
		}

		/*
		 * Is this a method inside of a class or a trait ? If so, it is a test class/trait ?
		 *
		 * {@internal Once the minimum supported PHPCS version has gone up to 3.1.0, the
		 * local array here can be replace with Tokens::$ooScopeTokens.}}
		 */
		$oo_tokens  = array(
			\T_CLASS      => true,
			\T_TRAIT      => true,
			\T_ANON_CLASS => true,
		);
		$conditions = $this->tokens[ $stackPtr ]['conditions'];

		foreach ( $conditions as $token => $condition ) {
			if ( $token === $functionToken ) {
				// Only examine the conditions the function is nested in, not those nested within the function.
				break;
			}

			if ( isset( $oo_tokens[ $condition ] ) ) {
				$is_test_class = $this->is_test_class( $token );
				if ( true === $is_test_class ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Check if a class token is part of a unit test suite.
	 *
	 * Unit test classes are identified as such:
	 * - Class which either extends WP_UnitTestCase or PHPUnit_Framework_TestCase
	 *   or a custom whitelisted unit test class.
	 *
	 * @since 0.12.0 Split off from the `is_token_in_test_method()` method.
	 * @since 1.0.0  Improved recognition of namespaced class names.
	 *
	 * @param int $stackPtr The position of the token to be examined.
	 *                      This should be a class, anonymous class or trait token.
	 *
	 * @return bool True if the class is a unit test class, false otherwise.
	 */
	protected function is_test_class( $stackPtr ) {

		if ( ! isset( $this->tokens[ $stackPtr ] )
			|| \in_array( $this->tokens[ $stackPtr ]['type'], array( 'T_CLASS', 'T_ANON_CLASS', 'T_TRAIT' ), true ) === false
		) {
			return false;
		}

		// Add any potentially whitelisted custom test classes to the whitelist.
		$whitelist = $this->merge_custom_array(
			$this->custom_test_class_whitelist,
			$this->test_class_whitelist
		);

		/*
		 * Show some tolerance for user input.
		 * The custom test class names should be passed as FQN without a prefixing `\`.
		 */
		foreach ( $whitelist as $k => $v ) {
			$whitelist[ $k ] = ltrim( $v, '\\' );
		}

		// Is the class/trait one of the whitelisted test classes ?
		$namespace = $this->determine_namespace( $stackPtr );
		$className = $this->phpcsFile->getDeclarationName( $stackPtr );
		if ( '' !== $namespace ) {
			if ( isset( $whitelist[ $namespace . '\\' . $className ] ) ) {
				return true;
			}
		} elseif ( isset( $whitelist[ $className ] ) ) {
			return true;
		}

		// Does the class/trait extend one of the whitelisted test classes ?
		$extendedClassName = $this->phpcsFile->findExtendedClassName( $stackPtr );
		if ( '\\' === $extendedClassName[0] ) {
			if ( isset( $whitelist[ substr( $extendedClassName, 1 ) ] ) ) {
				return true;
			}
		} elseif ( '' !== $namespace ) {
			if ( isset( $whitelist[ $namespace . '\\' . $extendedClassName ] ) ) {
				return true;
			}
		} elseif ( isset( $whitelist[ $extendedClassName ] ) ) {
			return true;
		}

		/*
		 * Not examining imported classes via `use` statements as with the variety of syntaxes,
		 * this would get very complicated.
		 * After all, users can add an `<exclude-pattern>` for a particular sniff to their
		 * custom ruleset to selectively exclude the test directory.
		 */

		return false;
	}

	/**
	 * Check if this variable is being assigned a value.
	 *
	 * E.g., $var = 'foo';
	 *
	 * Also handles array assignments to arbitrary depth:
	 *
	 * $array['key'][ $foo ][ something() ] = $bar;
	 *
	 * @since 0.5.0
	 *
	 * @param int $stackPtr The index of the token in the stack. This must point to
	 *                      either a T_VARIABLE or T_CLOSE_SQUARE_BRACKET token.
	 *
	 * @return bool Whether the token is a variable being assigned a value.
	 */
	protected function is_assignment( $stackPtr ) {

		static $valid = array(
			\T_VARIABLE             => true,
			\T_CLOSE_SQUARE_BRACKET => true,
		);

		// Must be a variable, constant or closing square bracket (see below).
		if ( ! isset( $valid[ $this->tokens[ $stackPtr ]['code'] ] ) ) {
			return false;
		}

		$next_non_empty = $this->phpcsFile->findNext(
			Tokens::$emptyTokens,
			( $stackPtr + 1 ),
			null,
			true,
			null,
			true
		);

		// No token found.
		if ( false === $next_non_empty ) {
			return false;
		}

		// If the next token is an assignment, that's all we need to know.
		if ( isset( Tokens::$assignmentTokens[ $this->tokens[ $next_non_empty ]['code'] ] ) ) {
			return true;
		}

		// Check if this is an array assignment, e.g., `$var['key'] = 'val';` .
		if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $next_non_empty ]['code'] ) {
			return $this->is_assignment( $this->tokens[ $next_non_empty ]['bracket_closer'] );
		}

		return false;
	}

	/**
	 * Check if this token has an associated nonce check.
	 *
	 * @since 0.5.0
	 *
	 * @param int $stackPtr The position of the current token in the stack of tokens.
	 *
	 * @return bool
	 */
	protected function has_nonce_check( $stackPtr ) {

		/**
		 * A cache of the scope that we last checked for nonce verification in.
		 *
		 * @var array {
		 *      @var string   $file        The name of the file.
		 *      @var int      $start       The index of the token where the scope started.
		 *      @var int      $end         The index of the token where the scope ended.
		 *      @var bool|int $nonce_check The index of the token where an nonce check
		 *                                 was found, or false if none was found.
		 * }
		 */
		static $last;

		$start = 0;
		$end   = $stackPtr;

		$tokens = $this->phpcsFile->getTokens();

		// If we're in a function, only look inside of it.
		$f = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
		if ( false !== $f ) {
			$start = $tokens[ $f ]['scope_opener'];
		} else {
			$f = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE );
			if ( false !== $f ) {
				$start = $tokens[ $f ]['scope_opener'];
			}
		}

		$in_isset = $this->is_in_isset_or_empty( $stackPtr );

		// We allow for isset( $_POST['var'] ) checks to come before the nonce check.
		// If this is inside an isset(), check after it as well, all the way to the
		// end of the scope.
		if ( $in_isset ) {
			$end = ( 0 === $start ) ? $this->phpcsFile->numTokens : $tokens[ $start ]['scope_closer'];
		}

		// Check if we've looked here before.
		$filename = $this->phpcsFile->getFilename();

		if (
			$filename === $last['file']
			&& $start === $last['start']
		) {

			if ( false !== $last['nonce_check'] ) {
				// If we have already found an nonce check in this scope, we just
				// need to check whether it comes before this token. It is OK if the
				// check is after the token though, if this was only a isset() check.
				return ( $in_isset || $last['nonce_check'] < $stackPtr );
			} elseif ( $end <= $last['end'] ) {
				// If not, we can still go ahead and return false if we've already
				// checked to the end of the search area.
				return false;
			}

			// We haven't checked this far yet, but we can still save work by
			// skipping over the part we've already checked.
			$start = $last['end'];
		} else {
			$last = array(
				'file'  => $filename,
				'start' => $start,
				'end'   => $end,
			);
		}

		// Loop through the tokens looking for nonce verification functions.
		for ( $i = $start; $i < $end; $i++ ) {

			// If this isn't a function name, skip it.
			if ( \T_STRING !== $tokens[ $i ]['code'] ) {
				continue;
			}

			// If this is one of the nonce verification functions, we can bail out.
			if ( isset( $this->nonceVerificationFunctions[ $tokens[ $i ]['content'] ] ) ) {
				$last['nonce_check'] = $i;
				return true;
			}
		}

		// We're still here, so no luck.
		$last['nonce_check'] = false;

		return false;
	}

	/**
	 * Check if a token is inside of an isset() or empty() statement.
	 *
	 * @since 0.5.0
	 *
	 * @param int $stackPtr The index of the token in the stack.
	 *
	 * @return bool Whether the token is inside an isset() or empty() statement.
	 */
	protected function is_in_isset_or_empty( $stackPtr ) {

		if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
			return false;
		}

		$nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis'];

		end( $nested_parenthesis );
		$open_parenthesis = key( $nested_parenthesis );

		$previous_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $open_parenthesis - 1 ), null, true, null, true );
		return in_array( $this->tokens[ $previous_non_empty ]['code'], array( \T_ISSET, \T_EMPTY ), true );
	}

	/**
	 * Check if something is only being sanitized.
	 *
	 * @since 0.5.0
	 *
	 * @param int $stackPtr The index of the token in the stack.
	 *
	 * @return bool Whether the token is only within a sanitization.
	 */
	protected function is_only_sanitized( $stackPtr ) {

		// If it isn't being sanitized at all.
		if ( ! $this->is_sanitized( $stackPtr ) ) {
			return false;
		}

		// If this isn't set, we know the value must have only been casted, because
		// is_sanitized() would have returned false otherwise.
		if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
			return true;
		}

		// At this point we're expecting the value to have not been casted. If it
		// was, it wasn't *only* casted, because it's also in a function.
		if ( $this->is_safe_casted( $stackPtr ) ) {
			return false;
		}

		// The only parentheses should belong to the sanitizing function. If there's
		// more than one set, this isn't *only* sanitization.
		return ( \count( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) === 1 );
	}

	/**
	 * Check if something is being casted to a safe value.
	 *
	 * @since 0.5.0
	 *
	 * @param int $stackPtr The index of the token in the stack.
	 *
	 * @return bool Whether the token being casted.
	 */
	protected function is_safe_casted( $stackPtr ) {

		// Get the last non-empty token.
		$prev = $this->phpcsFile->findPrevious(
			Tokens::$emptyTokens,
			( $stackPtr - 1 ),
			null,
			true
		);

		if ( false === $prev ) {
			return false;
		}

		// Check if it is a safe cast.
		return isset( $this->safe_casts[ $this->tokens[ $prev ]['code'] ] );
	}

	/**
	 * Check if something is being sanitized.
	 *
	 * @since 0.5.0
	 *
	 * @param int  $stackPtr        The index of the token in the stack.
	 * @param bool $require_unslash Whether to give an error if wp_unslash() isn't
	 *                              used on the variable before sanitization.
	 *
	 * @return bool Whether the token being sanitized.
	 */
	protected function is_sanitized( $stackPtr, $require_unslash = false ) {

		// First we check if it is being casted to a safe value.
		if ( $this->is_safe_casted( $stackPtr ) ) {
			return true;
		}

		// If this isn't within a function call, we know already that it's not safe.
		if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
			if ( $require_unslash ) {
				$this->add_unslash_error( $stackPtr );
			}
			return false;
		}

		// Get the function that it's in.
		$nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis'];
		$function_closer    = end( $nested_parenthesis );
		$function_opener    = key( $nested_parenthesis );
		$function           = $this->tokens[ ( $function_opener - 1 ) ];

		// If it is just being unset, the value isn't used at all, so it's safe.
		if ( \T_UNSET === $function['code'] ) {
			return true;
		}

		// If this isn't a call to a function, it sure isn't sanitizing function.
		if ( \T_STRING !== $function['code'] ) {
			if ( $require_unslash ) {
				$this->add_unslash_error( $stackPtr );
			}
			return false;
		}

		$functionName = $function['content'];

		// Check if wp_unslash() is being used.
		if ( 'wp_unslash' === $functionName ) {

			$is_unslashed    = true;
			$function_closer = prev( $nested_parenthesis );

			// If there is no other function being used, this value is unsanitized.
			if ( ! $function_closer ) {
				return false;
			}

			$function_opener = key( $nested_parenthesis );
			$functionName    = $this->tokens[ ( $function_opener - 1 ) ]['content'];

		} else {

			$is_unslashed = false;
		}

		// Arrays might be sanitized via array_map().
		if ( 'array_map' === $functionName ) {

			// Get the first parameter.
			$callback = $this->get_function_call_parameter( ( $function_opener - 1 ), 1 );

			if ( ! empty( $callback ) ) {
				/*
				 * If this is a function callback (not a method callback array) and we're able
				 * to resolve the function name, do so.
				 */
				$first_non_empty = $this->phpcsFile->findNext(
					Tokens::$emptyTokens,
					$callback['start'],
					( $callback['end'] + 1 ),
					true
				);

				if ( false !== $first_non_empty && \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $first_non_empty ]['code'] ) {
					$functionName = $this->strip_quotes( $this->tokens[ $first_non_empty ]['content'] );
				}
			}
		}

		// If slashing is required, give an error.
		if ( ! $is_unslashed && $require_unslash && ! isset( $this->unslashingSanitizingFunctions[ $functionName ] ) ) {
			$this->add_unslash_error( $stackPtr );
		}

		// Check if this is a sanitizing function.
		if ( isset( $this->sanitizingFunctions[ $functionName ] ) || isset( $this->unslashingSanitizingFunctions[ $functionName ] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Add an error for missing use of wp_unslash().
	 *
	 * @since 0.5.0
	 *
	 * @param int $stackPtr The index of the token in the stack.
	 */
	public function add_unslash_error( $stackPtr ) {

		$this->phpcsFile->addError(
			'Missing wp_unslash() before sanitization.',
			$stackPtr,
			'MissingUnslash',
			array( $this->tokens[ $stackPtr ]['content'] )
		);
	}

	/**
	 * Get the index key of an array variable.
	 *
	 * E.g., "bar" in $foo['bar'].
	 *
	 * @since 0.5.0
	 *
	 * @param int $stackPtr The index of the token in the stack.
	 *
	 * @return string|false The array index key whose value is being accessed.
	 */
	protected function get_array_access_key( $stackPtr ) {

		// Find the next non-empty token.
		$open_bracket = $this->phpcsFile->findNext(
			Tokens::$emptyTokens,
			( $stackPtr + 1 ),
			null,
			true
		);

		// If it isn't a bracket, this isn't an array-access.
		if ( false === $open_bracket || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code'] ) {
			return false;
		}

		$key = $this->phpcsFile->getTokensAsString(
			( $open_bracket + 1 ),
			( $this->tokens[ $open_bracket ]['bracket_closer'] - $open_bracket - 1 )
		);

		return trim( $key );
	}

	/**
	 * Check if the existence of a variable is validated with isset() or empty().
	 *
	 * When $in_condition_only is false, (which is the default), this is considered
	 * valid:
	 *
	 * ```php
	 * if ( isset( $var ) ) {
	 *     // Do stuff, like maybe return or exit (but could be anything)
	 * }
	 *
	 * foo( $var );
	 * ```
	 *
	 * When it is true, that would be invalid, the use of the variable must be within
	 * the scope of the validating condition, like this:
	 *
	 * ```php
	 * if ( isset( $var ) ) {
	 *     foo( $var );
	 * }
	 * ```
	 *
	 * @since 0.5.0
	 *
	 * @param int    $stackPtr          The index of this token in the stack.
	 * @param string $array_key         An array key to check for ("bar" in $foo['bar']).
	 * @param bool   $in_condition_only Whether to require that this use of the
	 *                                  variable occur within the scope of the
	 *                                  validating condition, or just in the same
	 *                                  scope as it (default).
	 *
	 * @return bool Whether the var is validated.
	 */
	protected function is_validated( $stackPtr, $array_key = null, $in_condition_only = false ) {

		if ( $in_condition_only ) {
			/*
			 * This is a stricter check, requiring the variable to be used only
			 * within the validation condition.
			 */

			// If there are no conditions, there's no validation.
			if ( empty( $this->tokens[ $stackPtr ]['conditions'] ) ) {
				return false;
			}

			$conditions = $this->tokens[ $stackPtr ]['conditions'];
			end( $conditions ); // Get closest condition.
			$conditionPtr = key( $conditions );
			$condition    = $this->tokens[ $conditionPtr ];

			if ( ! isset( $condition['parenthesis_opener'] ) ) {
				// Live coding or parse error.
				return false;
			}

			$scope_start = $condition['parenthesis_opener'];
			$scope_end   = $condition['parenthesis_closer'];

		} else {
			/*
			 * We are are more loose, requiring only that the variable be validated
			 * in the same function/file scope as it is used.
			 */

			$scope_start = 0;

			// Check if we are in a function.
			$function = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );

			// If so, we check only within the function, otherwise the whole file.
			if ( false !== $function ) {
				$scope_start = $this->tokens[ $function ]['scope_opener'];
			} else {
				// Check if we are in a closure.
				$closure = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE );

				// If so, we check only within the closure.
				if ( false !== $closure ) {
					$scope_start = $this->tokens[ $closure ]['scope_opener'];
				}
			}

			$scope_end = $stackPtr;

		}

		$bare_array_key = $this->strip_quotes( $array_key );

		for ( $i = ( $scope_start + 1 ); $i < $scope_end; $i++ ) {

			if ( ! \in_array( $this->tokens[ $i ]['code'], array( \T_ISSET, \T_EMPTY, \T_UNSET ), true ) ) {
				continue;
			}

			$issetOpener = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, $i );
			$issetCloser = $this->tokens[ $issetOpener ]['parenthesis_closer'];

			// Look for this variable. We purposely stomp $i from the parent loop.
			for ( $i = ( $issetOpener + 1 ); $i < $issetCloser; $i++ ) {

				if ( \T_VARIABLE !== $this->tokens[ $i ]['code'] ) {
					continue;
				}

				// If we're checking for a specific array key (ex: 'hello' in
				// $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
				if ( isset( $array_key )
					&& $this->strip_quotes( $this->get_array_access_key( $i ) ) !== $bare_array_key ) {
					continue;
				}

				return true;
			}
		}

		return false;
	}

	/**
	 * Check whether a variable is being compared to another value.
	 *
	 * E.g., $var === 'foo', 1 <= $var, etc.
	 *
	 * Also recognizes `switch ( $var )`.
	 *
	 * @since 0.5.0
	 *
	 * @param int $stackPtr The index of this token in the stack.
	 *
	 * @return bool Whether this is a comparison.
	 */
	protected function is_comparison( $stackPtr ) {

		// We first check if this is a switch statement (switch ( $var )).
		if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
			$nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis'];
			$close_parenthesis  = end( $nested_parenthesis );

			if (
				isset( $this->tokens[ $close_parenthesis ]['parenthesis_owner'] )
				&& \T_SWITCH === $this->tokens[ $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ]['code']
			) {
				return true;
			}
		}

		// Find the previous non-empty token. We check before the var first because
		// yoda conditions are usually expected.
		$previous_token = $this->phpcsFile->findPrevious(
			Tokens::$emptyTokens,
			( $stackPtr - 1 ),
			null,
			true
		);

		if ( isset( Tokens::$comparisonTokens[ $this->tokens[ $previous_token ]['code'] ] ) ) {
			return true;
		}

		// Maybe the comparison operator is after this.
		$next_token = $this->phpcsFile->findNext(
			Tokens::$emptyTokens,
			( $stackPtr + 1 ),
			null,
			true
		);

		// This might be an opening square bracket in the case of arrays ($var['a']).
		while ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $next_token ]['code'] ) {

			$next_token = $this->phpcsFile->findNext(
				Tokens::$emptyTokens,
				( $this->tokens[ $next_token ]['bracket_closer'] + 1 ),
				null,
				true
			);
		}

		if ( isset( Tokens::$comparisonTokens[ $this->tokens[ $next_token ]['code'] ] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check what type of 'use' statement a token is part of.
	 *
	 * The T_USE token has multiple different uses:
	 *
	 * 1. In a closure: function () use ( $var ) {}
	 * 2. In a class, to import a trait: use Trait_Name
	 * 3. In a namespace, to import a class: use Some\Class;
	 *
	 * This function will check the token and return 'closure', 'trait', or 'class',
	 * based on which of these uses the use is being used for.
	 *
	 * @since 0.7.0
	 *
	 * @param int $stackPtr The position of the token to check.
	 *
	 * @return string The type of use.
	 */
	protected function get_use_type( $stackPtr ) {

		// USE keywords inside closures.
		$next = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), null, true );

		if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] ) {
			return 'closure';
		}

		// USE keywords for traits.
		$valid_scopes = array(
			'T_CLASS'      => true,
			'T_ANON_CLASS' => true,
			'T_TRAIT'      => true,
		);
		if ( false !== $this->valid_direct_scope( $stackPtr, $valid_scopes ) ) {
			return 'trait';
		}

		// USE keywords for classes to import to a namespace.
		return 'class';
	}

	/**
	 * Get the interpolated variable names from a string.
	 *
	 * Check if '$' is followed by a valid variable name, and that it is not preceded by an escape sequence.
	 *
	 * @since 0.9.0
	 *
	 * @param string $string The contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token.
	 *
	 * @return array Variable names (without '$' sigil).
	 */
	protected function get_interpolated_variables( $string ) {
		$variables = array();
		if ( preg_match_all( '/(?P<backslashes>\\\\*)\$(?P<symbol>\w+)/', $string, $match_sets, \PREG_SET_ORDER ) ) {
			foreach ( $match_sets as $matches ) {
				if ( ! isset( $matches['backslashes'] ) || ( \strlen( $matches['backslashes'] ) % 2 ) === 0 ) {
					$variables[] = $matches['symbol'];
				}
			}
		}
		return $variables;
	}

	/**
	 * Strip variables from an arbitrary double quoted/heredoc string.
	 *
	 * Intended for use with the contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token.
	 *
	 * @since 0.14.0
	 *
	 * @param string $string The raw string.
	 *
	 * @return string String without variables in it.
	 */
	public function strip_interpolated_variables( $string ) {
		if ( strpos( $string, '$' ) === false ) {
			return $string;
		}

		return preg_replace( self::REGEX_COMPLEX_VARS, '', $string );
	}

	/**
	 * Checks if a function call has parameters.
	 *
	 * Expects to be passed the T_STRING stack pointer for the function call.
	 * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
	 *
	 * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer, it
	 * will detect whether the array has values or is empty.
	 *
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/120
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/152
	 *
	 * @since 0.11.0
	 *
	 * @param int $stackPtr The position of the function call token.
	 *
	 * @return bool
	 */
	public function does_function_call_have_parameters( $stackPtr ) {

		// Check for the existence of the token.
		if ( false === isset( $this->tokens[ $stackPtr ] ) ) {
			return false;
		}

		// Is this one of the tokens this function handles ?
		if ( false === \in_array( $this->tokens[ $stackPtr ]['code'], array( \T_STRING, \T_ARRAY, \T_OPEN_SHORT_ARRAY ), true ) ) {
			return false;
		}

		$next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );

		// Deal with short array syntax.
		if ( 'T_OPEN_SHORT_ARRAY' === $this->tokens[ $stackPtr ]['type'] ) {
			if ( false === isset( $this->tokens[ $stackPtr ]['bracket_closer'] ) ) {
				return false;
			}

			if ( $next_non_empty === $this->tokens[ $stackPtr ]['bracket_closer'] ) {
				// No parameters.
				return false;
			} else {
				return true;
			}
		}

		// Deal with function calls & long arrays.
		// Next non-empty token should be the open parenthesis.
		if ( false === $next_non_empty && \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] ) {
			return false;
		}

		if ( false === isset( $this->tokens[ $next_non_empty ]['parenthesis_closer'] ) ) {
			return false;
		}

		$close_parenthesis   = $this->tokens[ $next_non_empty ]['parenthesis_closer'];
		$next_next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), ( $close_parenthesis + 1 ), true );

		if ( $next_next_non_empty === $close_parenthesis ) {
			// No parameters.
			return false;
		}

		return true;
	}

	/**
	 * Count the number of parameters a function call has been passed.
	 *
	 * Expects to be passed the T_STRING stack pointer for the function call.
	 * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
	 *
	 * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer,
	 * it will return the number of values in the array.
	 *
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/111
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/114
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/151
	 *
	 * @since 0.11.0
	 *
	 * @param int $stackPtr The position of the function call token.
	 *
	 * @return int
	 */
	public function get_function_call_parameter_count( $stackPtr ) {
		if ( false === $this->does_function_call_have_parameters( $stackPtr ) ) {
			return 0;
		}

		return \count( $this->get_function_call_parameters( $stackPtr ) );
	}

	/**
	 * Get information on all parameters passed to a function call.
	 *
	 * Expects to be passed the T_STRING stack pointer for the function call.
	 * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
	 *
	 * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer,
	 * it will tokenize the values / key/value pairs contained in the array call.
	 *
	 * @since 0.11.0
	 *
	 * @param int $stackPtr The position of the function call token.
	 *
	 * @return array Multi-dimentional array with parameter details or
	 *               empty array if no parameters are found.
	 *
	 *               @type int $position 1-based index position of the parameter. {
	 *                   @type int $start Stack pointer for the start of the parameter.
	 *                   @type int $end   Stack pointer for the end of parameter.
	 *                   @type int $raw   Trimmed raw parameter content.
	 *               }
	 */
	public function get_function_call_parameters( $stackPtr ) {
		if ( false === $this->does_function_call_have_parameters( $stackPtr ) ) {
			return array();
		}

		/*
		 * Ok, we know we have a T_STRING, T_ARRAY or T_OPEN_SHORT_ARRAY with parameters
		 * and valid open & close brackets/parenthesis.
		 */

		// Mark the beginning and end tokens.
		if ( 'T_OPEN_SHORT_ARRAY' === $this->tokens[ $stackPtr ]['type'] ) {
			$opener = $stackPtr;
			$closer = $this->tokens[ $stackPtr ]['bracket_closer'];

			$nestedParenthesisCount = 0;
		} else {
			$opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
			$closer = $this->tokens[ $opener ]['parenthesis_closer'];

			$nestedParenthesisCount = 1;
		}

		// Which nesting level is the one we are interested in ?
		if ( isset( $this->tokens[ $opener ]['nested_parenthesis'] ) ) {
			$nestedParenthesisCount += \count( $this->tokens[ $opener ]['nested_parenthesis'] );
		}

		$parameters  = array();
		$next_comma  = $opener;
		$param_start = ( $opener + 1 );
		$cnt         = 1;
		while ( $next_comma = $this->phpcsFile->findNext( array( \T_COMMA, $this->tokens[ $closer ]['code'], \T_OPEN_SHORT_ARRAY, \T_CLOSURE ), ( $next_comma + 1 ), ( $closer + 1 ) ) ) {
			// Ignore anything within short array definition brackets.
			if ( 'T_OPEN_SHORT_ARRAY' === $this->tokens[ $next_comma ]['type']
				&& ( isset( $this->tokens[ $next_comma ]['bracket_opener'] )
					&& $this->tokens[ $next_comma ]['bracket_opener'] === $next_comma )
				&& isset( $this->tokens[ $next_comma ]['bracket_closer'] )
			) {
				// Skip forward to the end of the short array definition.
				$next_comma = $this->tokens[ $next_comma ]['bracket_closer'];
				continue;
			}

			// Skip past closures passed as function parameters.
			if ( 'T_CLOSURE' === $this->tokens[ $next_comma ]['type']
				&& ( isset( $this->tokens[ $next_comma ]['scope_condition'] )
					&& $this->tokens[ $next_comma ]['scope_condition'] === $next_comma )
				&& isset( $this->tokens[ $next_comma ]['scope_closer'] )
			) {
				// Skip forward to the end of the closure declaration.
				$next_comma = $this->tokens[ $next_comma ]['scope_closer'];
				continue;
			}

			// Ignore comma's at a lower nesting level.
			if ( \T_COMMA === $this->tokens[ $next_comma ]['code']
				&& isset( $this->tokens[ $next_comma ]['nested_parenthesis'] )
				&& \count( $this->tokens[ $next_comma ]['nested_parenthesis'] ) !== $nestedParenthesisCount
			) {
				continue;
			}

			// Ignore closing parenthesis/bracket if not 'ours'.
			if ( $this->tokens[ $next_comma ]['type'] === $this->tokens[ $closer ]['type'] && $next_comma !== $closer ) {
				continue;
			}

			// Ok, we've reached the end of the parameter.
			$parameters[ $cnt ]['start'] = $param_start;
			$parameters[ $cnt ]['end']   = ( $next_comma - 1 );
			$parameters[ $cnt ]['raw']   = trim( $this->phpcsFile->getTokensAsString( $param_start, ( $next_comma - $param_start ) ) );

			/*
			 * Check if there are more tokens before the closing parenthesis.
			 * Prevents code like the following from setting a third parameter:
			 * functionCall( $param1, $param2, );
			 */
			$has_next_param = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_comma + 1 ), $closer, true, null, true );
			if ( false === $has_next_param ) {
				break;
			}

			// Prepare for the next parameter.
			$param_start = ( $next_comma + 1 );
			$cnt++;
		}

		return $parameters;
	}

	/**
	 * Get information on a specific parameter passed to a function call.
	 *
	 * Expects to be passed the T_STRING stack pointer for the function call.
	 * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
	 *
	 * Will return a array with the start token pointer, end token pointer and the raw value
	 * of the parameter at a specific offset.
	 * If the specified parameter is not found, will return false.
	 *
	 * @since 0.11.0
	 *
	 * @param int $stackPtr     The position of the function call token.
	 * @param int $param_offset The 1-based index position of the parameter to retrieve.
	 *
	 * @return array|false
	 */
	public function get_function_call_parameter( $stackPtr, $param_offset ) {
		$parameters = $this->get_function_call_parameters( $stackPtr );

		if ( false === isset( $parameters[ $param_offset ] ) ) {
			return false;
		}

		return $parameters[ $param_offset ];
	}

	/**
	 * Find the array opener & closer based on a T_ARRAY or T_OPEN_SHORT_ARRAY token.
	 *
	 * @since 0.12.0
	 *
	 * @param int $stackPtr The stack pointer to the array token.
	 *
	 * @return array|bool Array with two keys `opener`, `closer` or false if
	 *                    either or these could not be determined.
	 */
	protected function find_array_open_close( $stackPtr ) {
		/*
		 * Determine the array opener & closer.
		 */
		if ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] ) {
			if ( isset( $this->tokens[ $stackPtr ]['parenthesis_opener'] ) ) {
				$opener = $this->tokens[ $stackPtr ]['parenthesis_opener'];

				if ( isset( $this->tokens[ $opener ]['parenthesis_closer'] ) ) {
					$closer = $this->tokens[ $opener ]['parenthesis_closer'];
				}
			}
		} else {
			// Short array syntax.
			$opener = $stackPtr;

			if ( isset( $this->tokens[ $stackPtr ]['bracket_closer'] ) ) {
				$closer = $this->tokens[ $stackPtr ]['bracket_closer'];
			}
		}

		if ( isset( $opener, $closer ) ) {
			return array(
				'opener' => $opener,
				'closer' => $closer,
			);
		}

		return false;
	}

	/**
	 * Determine the namespace name an arbitrary token lives in.
	 *
	 * @since 0.10.0
	 * @since 0.12.0 Moved from the WordPress_AbstractClassRestrictionsSniff to this sniff.
	 *
	 * @param int $stackPtr The token position for which to determine the namespace.
	 *
	 * @return string Namespace name or empty string if it couldn't be determined or no namespace applies.
	 */
	public function determine_namespace( $stackPtr ) {

		// Check for the existence of the token.
		if ( ! isset( $this->tokens[ $stackPtr ] ) ) {
			return '';
		}

		// Check for scoped namespace {}.
		if ( ! empty( $this->tokens[ $stackPtr ]['conditions'] ) ) {
			$namespacePtr = $this->phpcsFile->getCondition( $stackPtr, \T_NAMESPACE );
			if ( false !== $namespacePtr ) {
				$namespace = $this->get_declared_namespace_name( $namespacePtr );
				if ( false !== $namespace ) {
					return $namespace;
				}

				// We are in a scoped namespace, but couldn't determine the name.
				// Searching for a global namespace is futile.
				return '';
			}
		}

		/*
		 * Not in a scoped namespace, so let's see if we can find a non-scoped namespace instead.
		 * Keeping in mind that:
		 * - there can be multiple non-scoped namespaces in a file (bad practice, but it happens).
		 * - the namespace keyword can also be used as part of a function/method call and such.
		 * - that a non-named namespace resolves to the global namespace.
		 */
		$previousNSToken = $stackPtr;
		$namespace       = false;
		do {
			$previousNSToken = $this->phpcsFile->findPrevious( \T_NAMESPACE, ( $previousNSToken - 1 ) );

			// Stop if we encounter a scoped namespace declaration as we already know we're not in one.
			if ( ! empty( $this->tokens[ $previousNSToken ]['scope_condition'] )
				&& $this->tokens[ $previousNSToken ]['scope_condition'] === $previousNSToken
			) {
				break;
			}

			$namespace = $this->get_declared_namespace_name( $previousNSToken );

		} while ( false === $namespace && false !== $previousNSToken );

		// If we still haven't got a namespace, return an empty string.
		if ( false === $namespace ) {
			return '';
		}

		return $namespace;
	}

	/**
	 * Get the complete namespace name for a namespace declaration.
	 *
	 * For hierarchical namespaces, the name will be composed of several tokens,
	 * i.e. MyProject\Sub\Level which will be returned together as one string.
	 *
	 * @since 0.12.0 A lesser variant of this method previously existed in the
	 *               WordPress_AbstractClassRestrictionsSniff.
	 *
	 * @param int|bool $stackPtr The position of a T_NAMESPACE token.
	 *
	 * @return string|false Namespace name or false if not a namespace declaration.
	 *                      Namespace name can be an empty string for global namespace declaration.
	 */
	public function get_declared_namespace_name( $stackPtr ) {

		// Check for the existence of the token.
		if ( false === $stackPtr || ! isset( $this->tokens[ $stackPtr ] ) ) {
			return false;
		}

		if ( \T_NAMESPACE !== $this->tokens[ $stackPtr ]['code'] ) {
			return false;
		}

		$nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
		if ( \T_NS_SEPARATOR === $this->tokens[ $nextToken ]['code'] ) {
			// Not a namespace declaration, but use of, i.e. `namespace\someFunction();`.
			return false;
		}

		if ( \T_OPEN_CURLY_BRACKET === $this->tokens[ $nextToken ]['code'] ) {
			// Declaration for global namespace when using multiple namespaces in a file.
			// I.e.: `namespace {}`.
			return '';
		}

		// Ok, this should be a namespace declaration, so get all the parts together.
		$acceptedTokens = array(
			\T_STRING       => true,
			\T_NS_SEPARATOR => true,
		);
		$validTokens    = $acceptedTokens + Tokens::$emptyTokens;

		$namespaceName = '';
		while ( isset( $validTokens[ $this->tokens[ $nextToken ]['code'] ] ) ) {
			if ( isset( $acceptedTokens[ $this->tokens[ $nextToken ]['code'] ] ) ) {
				$namespaceName .= trim( $this->tokens[ $nextToken ]['content'] );
			}
			++$nextToken;
		}

		return $namespaceName;
	}

	/**
	 * Check if a content string contains a specific html open tag.
	 *
	 * @since 0.11.0
	 * @since 0.13.0 No longer allows for the PHP 5.2 bug for which the function was
	 *               originally created.
	 * @since 0.13.0 The $stackPtr parameter is now optional. Either that or the
	 *               $content parameter has to be passed.
	 * @deprecated 1.0.0 This method is only used by deprecated sniffs.
	 *
	 * @param string $tag_name The name of the HTML tag without brackets. So if
	 *                         searching for '<span...', this would be 'span'.
	 * @param int    $stackPtr Optional. The position of the current token in the
	 *                         token stack.
	 *                         This parameter needs to be passed if no $content is
	 *                         passed.
	 * @param string $content  Optionally, the current content string, might be a
	 *                         substring of the original string.
	 *                         Defaults to `false` to distinguish between a passed
	 *                         empty string and not passing the $content string.
	 *
	 * @return bool True if the string contains an <tag_name> open tag, false otherwise.
	 */
	public function has_html_open_tag( $tag_name, $stackPtr = null, $content = false ) {
		if ( false === $content && isset( $stackPtr ) ) {
			$content = $this->tokens[ $stackPtr ]['content'];
		}

		if ( ! empty( $content ) && false !== strpos( $content, '<' . $tag_name ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check whether a T_CONST token is a class constant declaration.
	 *
	 * @since 0.14.0
	 *
	 * @param int $stackPtr  The position in the stack of the T_CONST token to verify.
	 *
	 * @return bool
	 */
	public function is_class_constant( $stackPtr ) {
		if ( ! isset( $this->tokens[ $stackPtr ] ) || \T_CONST !== $this->tokens[ $stackPtr ]['code'] ) {
			return false;
		}

		// Note: traits can not declare constants.
		$valid_scopes = array(
			'T_CLASS'      => true,
			'T_ANON_CLASS' => true,
			'T_INTERFACE'  => true,
		);

		return is_int( $this->valid_direct_scope( $stackPtr, $valid_scopes ) );
	}

	/**
	 * Check whether a T_VARIABLE token is a class property declaration.
	 *
	 * @since 0.14.0
	 *
	 * @param int $stackPtr  The position in the stack of the T_VARIABLE token to verify.
	 *
	 * @return bool
	 */
	public function is_class_property( $stackPtr ) {
		if ( ! isset( $this->tokens[ $stackPtr ] ) || \T_VARIABLE !== $this->tokens[ $stackPtr ]['code'] ) {
			return false;
		}

		// Note: interfaces can not declare properties.
		$valid_scopes = array(
			'T_CLASS'      => true,
			'T_ANON_CLASS' => true,
			'T_TRAIT'      => true,
		);

		$scopePtr = $this->valid_direct_scope( $stackPtr, $valid_scopes );
		if ( false !== $scopePtr ) {
			// Make sure it's not a method parameter.
			if ( empty( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
				return true;
			} else {
				$parenthesis  = array_keys( $this->tokens[ $stackPtr ]['nested_parenthesis'] );
				$deepest_open = array_pop( $parenthesis );
				if ( $deepest_open < $scopePtr
					|| isset( $this->tokens[ $deepest_open ]['parenthesis_owner'] ) === false
					|| \T_FUNCTION !== $this->tokens[ $this->tokens[ $deepest_open ]['parenthesis_owner'] ]['code']
				) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Check whether the direct wrapping scope of a token is within a limited set of
	 * acceptable tokens.
	 *
	 * Used to check, for instance, if a T_CONST is a class constant.
	 *
	 * @since 0.14.0
	 *
	 * @param int   $stackPtr     The position in the stack of the token to verify.
	 * @param array $valid_scopes Array of token types.
	 *                            Keys should be the token types in string format
	 *                            to allow for newer token types.
	 *                            Value is irrelevant.
	 *
	 * @return int|bool StackPtr to the scope if valid, false otherwise.
	 */
	protected function valid_direct_scope( $stackPtr, array $valid_scopes ) {
		if ( empty( $this->tokens[ $stackPtr ]['conditions'] ) ) {
			return false;
		}

		/*
		 * Check only the direct wrapping scope of the token.
		 */
		$conditions = array_keys( $this->tokens[ $stackPtr ]['conditions'] );
		$ptr        = array_pop( $conditions );

		if ( ! isset( $this->tokens[ $ptr ] ) ) {
			return false;
		}

		if ( isset( $valid_scopes[ $this->tokens[ $ptr ]['type'] ] ) ) {
			return $ptr;
		}

		return false;
	}

	/**
	 * Checks whether this is a call to a $wpdb method that we want to sniff.
	 *
	 * If available in the child class, the $methodPtr, $i and $end properties are
	 * automatically set to correspond to the start and end of the method call.
	 * The $i property is also set if this is not a method call but rather the
	 * use of a $wpdb property.
	 *
	 * @since 0.8.0
	 * @since 0.9.0  The return value is now always boolean. The $end and $i member
	 *               vars are automatically updated.
	 * @since 0.14.0 Moved this method from the `PreparedSQL` sniff to the base WP sniff.
	 *
	 * {@internal This method should probably be refactored.}}
	 *
	 * @param int   $stackPtr        The index of the $wpdb variable.
	 * @param array $target_methods  Array of methods. Key(s) should be method name.
	 *
	 * @return bool Whether this is a $wpdb method call.
	 */
	protected function is_wpdb_method_call( $stackPtr, $target_methods ) {

		// Check for wpdb.
		if ( ( \T_VARIABLE === $this->tokens[ $stackPtr ]['code'] && '$wpdb' !== $this->tokens[ $stackPtr ]['content'] )
			|| ( \T_STRING === $this->tokens[ $stackPtr ]['code'] && 'wpdb' !== $this->tokens[ $stackPtr ]['content'] )
		) {
			return false;
		}

		// Check that this is a method call.
		$is_object_call = $this->phpcsFile->findNext(
			array( \T_OBJECT_OPERATOR, \T_DOUBLE_COLON ),
			( $stackPtr + 1 ),
			null,
			false,
			null,
			true
		);
		if ( false === $is_object_call ) {
			return false;
		}

		$methodPtr = $this->phpcsFile->findNext( \T_WHITESPACE, ( $is_object_call + 1 ), null, true, null, true );
		if ( false === $methodPtr ) {
			return false;
		}

		if ( \T_STRING === $this->tokens[ $methodPtr ]['code'] && property_exists( $this, 'methodPtr' ) ) {
			$this->methodPtr = $methodPtr;
		}

		// Find the opening parenthesis.
		$opening_paren = $this->phpcsFile->findNext( \T_WHITESPACE, ( $methodPtr + 1 ), null, true, null, true );

		if ( false === $opening_paren ) {
			return false;
		}

		if ( property_exists( $this, 'i' ) ) {
			$this->i = $opening_paren;
		}

		if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $opening_paren ]['code']
			|| ! isset( $this->tokens[ $opening_paren ]['parenthesis_closer'] )
		) {
			return false;
		}

		// Check that this is one of the methods that we are interested in.
		if ( ! isset( $target_methods[ $this->tokens[ $methodPtr ]['content'] ] ) ) {
			return false;
		}

		// Find the end of the first parameter.
		$end = $this->phpcsFile->findEndOfStatement( $opening_paren + 1 );

		if ( \T_COMMA !== $this->tokens[ $end ]['code'] ) {
			++$end;
		}

		if ( property_exists( $this, 'end' ) ) {
			$this->end = $end;
		}

		return true;
	}

	/**
	 * Determine whether an arbitrary T_STRING token is the use of a global constant.
	 *
	 * @since 1.0.0
	 *
	 * @param int $stackPtr The position of the function call token.
	 *
	 * @return bool
	 */
	public function is_use_of_global_constant( $stackPtr ) {
		// Check for the existence of the token.
		if ( ! isset( $this->tokens[ $stackPtr ] ) ) {
			return false;
		}

		// Is this one of the tokens this function handles ?
		if ( \T_STRING !== $this->tokens[ $stackPtr ]['code'] ) {
			return false;
		}

		$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
		if ( false !== $next
			&& ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code']
				|| \T_DOUBLE_COLON === $this->tokens[ $next ]['code'] )
		) {
			// Function call or declaration.
			return false;
		}

		// Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a global constant.
		$tokens_to_ignore = array(
			'T_NAMESPACE'       => true,
			'T_USE'             => true,
			'T_CLASS'           => true,
			'T_TRAIT'           => true,
			'T_INTERFACE'       => true,
			'T_EXTENDS'         => true,
			'T_IMPLEMENTS'      => true,
			'T_NEW'             => true,
			'T_FUNCTION'        => true,
			'T_DOUBLE_COLON'    => true,
			'T_OBJECT_OPERATOR' => true,
			'T_INSTANCEOF'      => true,
			'T_INSTEADOF'       => true,
			'T_GOTO'            => true,
			'T_AS'              => true,
			'T_PUBLIC'          => true,
			'T_PROTECTED'       => true,
			'T_PRIVATE'         => true,
		);

		$prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
		if ( false !== $prev
			&& isset( $tokens_to_ignore[ $this->tokens[ $prev ]['type'] ] )
		) {
			// Not the use of a constant.
			return false;
		}

		if ( false !== $prev
			&& \T_NS_SEPARATOR === $this->tokens[ $prev ]['code']
			&& \T_STRING === $this->tokens[ ( $prev - 1 ) ]['code']
		) {
			// Namespaced constant of the same name.
			return false;
		}

		if ( false !== $prev
			&& \T_CONST === $this->tokens[ $prev ]['code']
			&& $this->is_class_constant( $prev )
		) {
			// Class constant declaration of the same name.
			return false;
		}

		/*
		 * Deal with a number of variations of use statements.
		 */
		for ( $i = $stackPtr; $i > 0; $i-- ) {
			if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $stackPtr ]['line'] ) {
				break;
			}
		}

		$firstOnLine = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
		if ( false !== $firstOnLine && \T_USE === $this->tokens[ $firstOnLine ]['code'] ) {
			$nextOnLine = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $firstOnLine + 1 ), null, true );
			if ( false !== $nextOnLine ) {
				if ( \T_STRING === $this->tokens[ $nextOnLine ]['code']
					&& 'const' === $this->tokens[ $nextOnLine ]['content']
				) {
					$hasNsSep = $this->phpcsFile->findNext( \T_NS_SEPARATOR, ( $nextOnLine + 1 ), $stackPtr );
					if ( false !== $hasNsSep ) {
						// Namespaced const (group) use statement.
						return false;
					}
				} else {
					// Not a const use statement.
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Determine if a variable is in the `as $key => $value` part of a foreach condition.
	 *
	 * @since 1.0.0
	 * @since 1.1.0 Moved from the PrefixAllGlobals sniff to the Sniff base class.
	 *
	 * @param int $stackPtr Pointer to the variable.
	 *
	 * @return bool True if it is. False otherwise.
	 */
	protected function is_foreach_as( $stackPtr ) {
		if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
			return false;
		}

		$nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis'];
		$close_parenthesis  = end( $nested_parenthesis );
		$open_parenthesis   = key( $nested_parenthesis );
		if ( ! isset( $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ) ) {
			return false;
		}

		if ( \T_FOREACH !== $this->tokens[ $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ]['code'] ) {
			return false;
		}

		$as_ptr = $this->phpcsFile->findNext( \T_AS, ( $open_parenthesis + 1 ), $close_parenthesis );
		if ( false === $as_ptr ) {
			// Should never happen.
			return false;
		}

		return ( $stackPtr > $as_ptr );
	}

}
