Skip to content
Permalink
d45f5e5cf3
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
3422 lines (3049 sloc) 110 KB
<?php
/**
* Represents a PHP_CodeSniffer sniff for sniffing WordPress coding standards.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress;
use PHP_CodeSniffer\Sniffs\Sniff as PHPCS_Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use WordPressCS\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 = '5.0';
/**
* 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">
* <element value="My_Plugin_First_Test_Class"/>
* <element value="My_Plugin_Second_Test_Class"/>
* </property>
* </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,
'highlight_string' => true,
'intval' => true,
'json_encode' => true,
'like_escape' => true,
'number_format' => true,
'rawurlencode' => true,
'sanitize_html_class' => true,
'sanitize_key' => 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,
'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,
'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,
'esc_url_raw' => true,
'filter_input' => true,
'filter_var' => true,
'hash_equals' => 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_sanitize_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,
'count' => true,
'doubleval' => true,
'floatval' => true,
'intval' => true,
'sanitize_key' => true,
'sizeof' => true,
);
/**
* Functions which unslash the data passed to them.
*
* @since 2.1.0
*
* @var array
*/
protected $unslashingFunctions = array(
'stripslashes_deep' => true,
'stripslashes_from_strings_only' => true,
'wp_unslash' => true,
);
/**
* List of PHP native functions to test the type of a variable.
*
* Using these functions is safe in combination with superglobals without
* unslashing or sanitization.
*
* They should, however, not be regarded as unslashing or sanitization functions.
*
* @since 2.1.0
*
* @var array
*/
protected $typeTestFunctions = array(
'is_array' => true,
'is_bool' => true,
'is_callable' => true,
'is_countable' => true,
'is_double' => true,
'is_float' => true,
'is_int' => true,
'is_integer' => true,
'is_iterable' => true,
'is_long' => true,
'is_null' => true,
'is_numeric' => true,
'is_object' => true,
'is_real' => true,
'is_resource' => true,
'is_scalar' => true,
'is_string' => 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,
\T_UNSET_CAST => true,
);
/**
* List of array functions which apply a callback to the array.
*
* These are often used for sanitization/escaping an array variable.
*
* Note: functions which alter the array by reference are not listed here on purpose.
* These cannot easily be used for sanitization as they can't be combined with unslashing.
* Similarly, they cannot be used for late escaping as the return value is a boolean, not
* the altered array.
*
* @since 2.1.0
*
* @var array <string function name> => <int parameter position of the callback parameter>
*/
protected $arrayWalkingFunctions = array(
'array_map' => 1,
'map_deep' => 2,
);
/**
* Array functions to compare a $needle to a predefined set of values.
*
* If the value is set to an integer, the function needs to have at least that
* many parameters for it to be considered as a comparison.
*
* @since 2.1.0
*
* @var array <string function name> => <true|int>
*/
protected $arrayCompareFunctions = array(
'in_array' => true,
'array_search' => true,
'array_keys' => 2,
);
/**
* 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 here from the WordPress.Variables.GlobalVariables 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,
'content_width' => true,
'current_blog' => 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,
'mu_plugin' => true,
'multipage' => true,
'names' => true,
'nav_menu_selected_id' => true,
'network_plugin' => 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,
'plugin' => 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,
'tag_ID' => 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_Base' => true,
'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,
// PHPUnit native TestCase class when imported via use statement.
'TestCase' => 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 );
}
/**
* Transform the name of a PHP construct (function, variable etc) to one in snake_case.
*
* @since 2.0.0 Moved from the `WordPress.NamingConventions.ValidFunctionName` sniff
* to this class, renamed from `get_name_suggestion` and made static
* so it can also be used by classes which don't extend this class.
*
* @param string $name The construct name.
*
* @return string
*/
public static function get_snake_case_name_suggestion( $name ) {
$suggested = preg_replace( '`([A-Z])`', '_$1', $name );
$suggested = strtolower( $suggested );
$suggested = str_replace( '__', '_', $suggested );
$suggested = trim( $suggested, '_' );
return $suggested;
}
/**
* Merge a pre-set array with a ruleset provided array.
*
* - 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.NamingConventions.ValidVariableName` sniff
* which extends an upstream sniff can also use it.}}
*
* @since 0.11.0
* @since 2.0.0 No longer supports custom array properties which were incorrectly
* passed as a string.
*
* @param array $custom Custom list as provided via a ruleset.
* @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 ) ) {
return $base;
}
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.
*
* @deprecated 2.0.0 Use the PHPCS native `phpcs:ignore` annotations instead.
*
* @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;
}
static $thrown_notices = array();
$deprecation_notice = 'Using the WPCS native whitelist comments is deprecated. Please use the PHPCS native "phpcs:ignore Standard.Category.SniffName.ErrorCode" annotations instead. Found: %s';
$deprecation_code = 'DeprecatedWhitelistCommentFound';
$filename = $this->phpcsFile->getFileName();
$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']
|| ( isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $lastPtr ]['code'] ] )
&& \T_PHPCS_SET !== $this->tokens[ $lastPtr ]['code'] ) )
&& $this->tokens[ $lastPtr ]['line'] === $this->tokens[ $end_of_statement ]['line']
&& preg_match( $regex, $this->tokens[ $lastPtr ]['content'] ) === 1
) {
if ( isset( $thrown_notices[ $filename ][ $lastPtr ] ) === false
&& isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $lastPtr ]['code'] ] ) === false
) {
$this->phpcsFile->addWarning(
$deprecation_notice,
$lastPtr,
$deprecation_code,
array( $this->tokens[ $lastPtr ]['content'] )
);
$thrown_notices[ $filename ][ $lastPtr ] = true;
}
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']
|| ( isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $lastPtr ]['code'] ] )
&& \T_PHPCS_SET !== $this->tokens[ $lastPtr ]['code'] ) )
&& $this->tokens[ $lastPtr ]['line'] === $this->tokens[ $stackPtr ]['line']
&& preg_match( $regex, $this->tokens[ $lastPtr ]['content'] ) === 1
) {
if ( isset( $thrown_notices[ $filename ][ $lastPtr ] ) === false
&& isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $lastPtr ]['code'] ] ) === false
) {
$this->phpcsFile->addWarning(
$deprecation_notice,
$lastPtr,
$deprecation_code,
array( $this->tokens[ $lastPtr ]['content'] )
);
$thrown_notices[ $filename ][ $lastPtr ] = true;
}
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;
}
$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( Tokens::$ooScopeTokens[ $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 ], Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] ) === 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 ( false === $extendedClassName ) {
return false;
}
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']
&& isset( $this->tokens[ $next_non_empty ]['bracket_closer'] )
) {
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.
// Once PHPCS 3.5.0 comes out this should be changed to the new Conditions::GetLastCondition() method.
if ( isset( $tokens[ $stackPtr ]['conditions'] ) === true ) {
$conditions = $tokens[ $stackPtr ]['conditions'];
$conditions = array_reverse( $conditions, true );
foreach ( $conditions as $tokenPtr => $condition ) {
if ( \T_FUNCTION === $condition || \T_CLOSURE === $condition ) {
$start = $tokens[ $tokenPtr ]['scope_opener'];
break;
}
}
}
$allow_nonce_after = false;
if ( $this->is_in_isset_or_empty( $stackPtr )
|| $this->is_in_type_test( $stackPtr )
|| $this->is_comparison( $stackPtr )
|| $this->is_in_array_comparison( $stackPtr )
|| $this->is_in_function_call( $stackPtr, $this->unslashingFunctions ) !== false
|| $this->is_only_sanitized( $stackPtr )
) {
$allow_nonce_after = true;
}
// We allow for certain actions, such as an isset() check to come before the nonce check.
// If this superglobal is inside such a check, look for the nonce after it as well,
// all the way to the end of the scope.
if ( true === $allow_nonce_after ) {
$end = ( 0 === $start ) ? $this->phpcsFile->numTokens : $tokens[ $start ]['scope_closer'];
}
// Check if we've looked here before.
$filename = $this->phpcsFile->getFilename();
if ( is_array( $last )
&& $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 ( true === $allow_nonce_after || $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++ ) {
// Skip over nested closed scope constructs.
if ( \T_FUNCTION === $tokens[ $i ]['code']
|| \T_CLOSURE === $tokens[ $i ]['code']
|| isset( Tokens::$ooScopeTokens[ $tokens[ $i ]['code'] ] )
) {
if ( isset( $tokens[ $i ]['scope_closer'] ) ) {
$i = $tokens[ $i ]['scope_closer'];
}
continue;
}
// 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'] ] ) ) {
/*
* Now, make sure it is a call to a global function.
*/
if ( $this->is_class_object_call( $i ) === true ) {
continue;
}
if ( $this->is_token_namespaced( $i ) === true ) {
continue;
}
$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(), empty() or array_key_exists() statement.
*
* @since 0.5.0
* @since 2.1.0 Now checks for the token being used as the array parameter
* in function calls to array_key_exists() and key_exists() as well.
*
* @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 );
if ( false === $previous_non_empty ) {
return false;
}
$previous_code = $this->tokens[ $previous_non_empty ]['code'];
if ( \T_ISSET === $previous_code || \T_EMPTY === $previous_code ) {
return true;
}
$valid_functions = array(
'array_key_exists' => true,
'key_exists' => true, // Alias.
);
$functionPtr = $this->is_in_function_call( $stackPtr, $valid_functions );
if ( false !== $functionPtr ) {
$second_param = $this->get_function_call_parameter( $functionPtr, 2 );
if ( $stackPtr >= $second_param['start'] && $stackPtr <= $second_param['end'] ) {
return true;
}
}
return false;
}
/**
* Check if a particular token is a (static or non-static) call to a class method or property.
*
* @internal Note: this may still mistake a namespaced function imported via a `use` statement for
* a global function!
*
* @since 2.1.0
*
* @param int $stackPtr The index of the token in the stack.
*
* @return bool
*/
protected function is_class_object_call( $stackPtr ) {
$before = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true, null, true );
if ( false === $before ) {
return false;
}
if ( \T_OBJECT_OPERATOR !== $this->tokens[ $before ]['code']
&& \T_DOUBLE_COLON !== $this->tokens[ $before ]['code']
) {
return false;
}
return true;
}
/**
* Check if a particular token is prefixed with a namespace.
*
* @internal This will give a false positive if the file is not namespaced and the token is prefixed
* with `namespace\`.
*
* @since 2.1.0
*
* @param int $stackPtr The index of the token in the stack.
*
* @return bool
*/
protected function is_token_namespaced( $stackPtr ) {
$prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true, null, true );
if ( false === $prev ) {
return false;
}
if ( \T_NS_SEPARATOR !== $this->tokens[ $prev ]['code'] ) {
return false;
}
$before_prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true, null, true );
if ( false === $before_prev ) {
return false;
}
if ( \T_STRING !== $this->tokens[ $before_prev ]['code']
&& \T_NAMESPACE !== $this->tokens[ $before_prev ]['code']
) {
return false;
}
return true;
}
/**
* Check if a token is (part of) a parameter for a function call to a select list of functions.
*
* This is useful, for instance, when trying to determine the context a variable is used in.
*
* For example: this function could be used to determine if the variable `$foo` is used
* in a global function call to the function `is_foo()`.
* In that case, a call to this function would return the stackPtr to the T_STRING `is_foo`
* for code like: `is_foo( $foo, 'some_other_param' )`, while it would return `false` for
* the following code `is_bar( $foo, 'some_other_param' )`.
*
* @since 2.1.0
*
* @param int $stackPtr The index of the token in the stack.
* @param array $valid_functions List of valid function names.
* Note: The keys to this array should be the function names
* in lowercase. Values are irrelevant.
* @param bool $global Optional. Whether to make sure that the function call is
* to a global function. If `false`, calls to methods, be it static
* `Class::method()` or via an object `$obj->method()`, and
* namespaced function calls, like `MyNS\function_name()` will
* also be accepted.
* Defaults to `true`.
* @param bool $allow_nested Optional. Whether to allow for nested function calls within the
* call to this function.
* I.e. when checking whether a token is within a function call
* to `strtolower()`, whether to accept `strtolower( trim( $var ) )`
* or only `strtolower( $var )`.
* Defaults to `false`.
*
* @return int|bool Stack pointer to the function call T_STRING token or false otherwise.
*/
protected function is_in_function_call( $stackPtr, $valid_functions, $global = true, $allow_nested = false ) {
if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
return false;
}
$nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis'];
if ( false === $allow_nested ) {
$nested_parenthesis = array_reverse( $nested_parenthesis, true );
}
foreach ( $nested_parenthesis as $open => $close ) {
$prev_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $open - 1 ), null, true, null, true );
if ( false === $prev_non_empty || \T_STRING !== $this->tokens[ $prev_non_empty ]['code'] ) {
continue;
}
if ( isset( $valid_functions[ strtolower( $this->tokens[ $prev_non_empty ]['content'] ) ] ) === false ) {
if ( false === $allow_nested ) {
// Function call encountered, but not to one of the allowed functions.
return false;
}
continue;
}
if ( false === $global ) {
return $prev_non_empty;
}
/*
* Now, make sure it is a global function.
*/
if ( $this->is_class_object_call( $prev_non_empty ) === true ) {
continue;
}
if ( $this->is_token_namespaced( $prev_non_empty ) === true ) {
continue;
}
return $prev_non_empty;
}
return false;
}
/**
* Check if a token is inside of an is_...() statement.
*
* @since 2.1.0
*
* @param int $stackPtr The index of the token in the stack.
*
* @return bool Whether the token is being type tested.
*/
protected function is_in_type_test( $stackPtr ) {
/*
* Casting the potential integer stack pointer return value to boolean here is fine.
* The return can never be `0` as there will always be a PHP open tag before the
* function call.
*/
return (bool) $this->is_in_function_call( $stackPtr, $this->typeTestFunctions );
}
/**
* 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 no unslashing function
* is 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'];
$nested_openers = array_keys( $nested_parenthesis );
$function_opener = array_pop( $nested_openers );
$functionPtr = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $function_opener - 1 ), null, true, null, true );
// If it is just being unset, the value isn't used at all, so it's safe.
if ( \T_UNSET === $this->tokens[ $functionPtr ]['code'] ) {
return true;
}
$valid_functions = $this->sanitizingFunctions;
$valid_functions += $this->unslashingSanitizingFunctions;
$valid_functions += $this->unslashingFunctions;
$valid_functions += $this->arrayWalkingFunctions;
$functionPtr = $this->is_in_function_call( $stackPtr, $valid_functions );
// If this isn't a call to one of the valid functions, it sure isn't a sanitizing function.
if ( false === $functionPtr ) {
if ( true === $require_unslash ) {
$this->add_unslash_error( $stackPtr );
}
return false;
}
$functionName = $this->tokens[ $functionPtr ]['content'];
// Check if an unslashing function is being used.
if ( isset( $this->unslashingFunctions[ $functionName ] ) ) {
$is_unslashed = true;
// Remove the unslashing functions.
$valid_functions = array_diff_key( $valid_functions, $this->unslashingFunctions );
// Check is any of the remaining (sanitizing) functions is used.
$higherFunctionPtr = $this->is_in_function_call( $functionPtr, $valid_functions );
// If there is no other valid function being used, this value is unsanitized.
if ( false === $higherFunctionPtr ) {
return false;
}
$functionPtr = $higherFunctionPtr;
$functionName = $this->tokens[ $functionPtr ]['content'];
} else {
$is_unslashed = false;
}
// Arrays might be sanitized via an array walking function using a callback.
if ( isset( $this->arrayWalkingFunctions[ $functionName ] ) ) {
// Get the callback parameter.
$callback = $this->get_function_call_parameter( $functionPtr, $this->arrayWalkingFunctions[ $functionName ] );
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 unslashing.
*
* @since 0.5.0
*
* @param int $stackPtr The index of the token in the stack.
*/
public function add_unslash_error( $stackPtr ) {
$this->phpcsFile->addError(
'%s data not unslashed before sanitization. Use wp_unslash() or similar',
$stackPtr,
'MissingUnslash',
array( $this->tokens[ $stackPtr ]['content'] )
);
}
/**
* Get the index keys of an array variable.
*
* E.g., "bar" and "baz" in $foo['bar']['baz'].
*
* @since 2.1.0
*
* @param int $stackPtr The index of the variable token in the stack.
* @param bool $all Whether to get all keys or only the first.
* Defaults to `true`(= all).
*
* @return array An array of index keys whose value is being accessed.
* or an empty array if this is not array access.
*/
protected function get_array_access_keys( $stackPtr, $all = true ) {
$keys = array();
if ( \T_VARIABLE !== $this->tokens[ $stackPtr ]['code'] ) {
return $keys;
}
$current = $stackPtr;
do {
// Find the next non-empty token.
$open_bracket = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
( $current + 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']
|| ! isset( $this->tokens[ $open_bracket ]['bracket_closer'] )
) {
break;
}
$key = $this->phpcsFile->getTokensAsString(
( $open_bracket + 1 ),
( $this->tokens[ $open_bracket ]['bracket_closer'] - $open_bracket - 1 )
);
$keys[] = trim( $key );
$current = $this->tokens[ $open_bracket ]['bracket_closer'];
} while ( isset( $this->tokens[ $current ] ) && true === $all );
return $keys;
}
/**
* Get the index key of an array variable.
*
* E.g., "bar" in $foo['bar'].
*
* @since 0.5.0
* @since 2.1.0 Now uses get_array_access_keys() under the hood.
*
* @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 ) {
$keys = $this->get_array_access_keys( $stackPtr, false );
if ( isset( $keys[0] ) ) {
return $keys[0];
}
return false;
}
/**
* Check if the existence of a variable is validated with isset(), empty(), array_key_exists()
* or key_exists().
*
* 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
* @since 2.1.0 Now recognizes array_key_exists() and key_exists() as validation functions.
* @since 2.1.0 Stricter check on whether the correct variable and the correct
* array keys are being validated.
*
* @param int $stackPtr The index of this token in the stack.
* @param array|string $array_keys An array key to check for ("bar" in $foo['bar'])
* or an array of keys for multi-level array access.
* @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_keys = array(), $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;
}
if ( ! empty( $array_keys ) && ! is_array( $array_keys ) ) {
$array_keys = (array) $array_keys;
}
$bare_array_keys = array_map( array( $this, 'strip_quotes' ), $array_keys );
$targets = array(
\T_ISSET => 'construct',
\T_EMPTY => 'construct',
\T_UNSET => 'construct',
\T_STRING => 'function_call',
\T_COALESCE => 'coalesce',
\T_COALESCE_EQUAL => 'coalesce',
);
// phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer.Found -- On purpose, see below.
for ( $i = ( $scope_start + 1 ); $i < $scope_end; $i++ ) {
if ( isset( $targets[ $this->tokens[ $i ]['code'] ] ) === false ) {
continue;
}
switch ( $targets[ $this->tokens[ $i ]['code'] ] ) {
case 'construct':
$issetOpener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true, null, true );
if ( false === $issetOpener || \T_OPEN_PARENTHESIS !== $this->tokens[ $issetOpener ]['code'] ) {
// Parse error or live coding.
continue 2;
}
$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 ( $this->tokens[ $stackPtr ]['content'] !== $this->tokens[ $i ]['content'] ) {
continue;
}
// If we're checking for specific array keys (ex: 'hello' in
// $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
if ( ! empty( $bare_array_keys ) ) {
$found_keys = $this->get_array_access_keys( $i );
$found_keys = array_map( array( $this, 'strip_quotes' ), $found_keys );
$diff = array_diff_assoc( $bare_array_keys, $found_keys );
if ( ! empty( $diff ) ) {
continue;
}
}
return true;
}
break;
case 'function_call':
// Only check calls to array_key_exists() and key_exists().
if ( 'array_key_exists' !== $this->tokens[ $i ]['content']
&& 'key_exists' !== $this->tokens[ $i ]['content']
) {
continue 2;
}
$next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true, null, true );
if ( false === $next_non_empty || \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] ) {
// Not a function call.
continue 2;
}
if ( $this->is_class_object_call( $i ) === true ) {
// Method call.
continue 2;
}
if ( $this->is_token_namespaced( $i ) === true ) {
// Namespaced function call.
continue 2;
}
$params = $this->get_function_call_parameters( $i );
if ( count( $params ) < 2 ) {
continue 2;
}
$param2_first_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, $params[2]['start'], ( $params[2]['end'] + 1 ), true );
if ( false === $param2_first_token
|| \T_VARIABLE !== $this->tokens[ $param2_first_token ]['code']
|| $this->tokens[ $param2_first_token ]['content'] !== $this->tokens[ $stackPtr ]['content']
) {
continue 2;
}
if ( ! empty( $bare_array_keys ) ) {
// Prevent the original array from being altered.
$bare_keys = $bare_array_keys;
$last_key = array_pop( $bare_keys );
/*
* For multi-level array access, the complete set of keys could be split between
* the first and the second parameter, but could also be completely in the second
* parameter, so we need to check both options.
*/
$found_keys = $this->get_array_access_keys( $param2_first_token );
$found_keys = array_map( array( $this, 'strip_quotes' ), $found_keys );
// First try matching the complete set against the second parameter.
$diff = array_diff_assoc( $bare_array_keys, $found_keys );
if ( empty( $diff ) ) {
return true;
}
// If that failed, try getting an exact match for the subset against the
// second parameter and the last key against the first.
if ( $bare_keys === $found_keys && $this->strip_quotes( $params[1]['raw'] ) === $last_key ) {
return true;
}
// Didn't find the correct array keys.
continue 2;
}
return true;
case 'coalesce':
$prev = $i;
do {
$prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true, null, true );
// Skip over array keys, like $_GET['key']['subkey'].
if ( \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $prev ]['code'] ) {
$prev = $this->tokens[ $prev ]['bracket_opener'];
continue;
}
break;
} while ( $prev >= ( $scope_start + 1 ) );
// We should now have reached the variable.
if ( \T_VARIABLE !== $this->tokens[ $prev ]['code'] ) {
continue 2;
}
if ( $this->tokens[ $prev ]['content'] !== $this->tokens[ $stackPtr ]['content'] ) {
continue 2;
}
if ( ! empty( $bare_array_keys ) ) {
$found_keys = $this->get_array_access_keys( $prev );
$found_keys = array_map( array( $this, 'strip_quotes' ), $found_keys );
$diff = array_diff_assoc( $bare_array_keys, $found_keys );
if ( ! empty( $diff ) ) {
continue 2;
}
}
// Right variable, correct key.
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
* @since 2.1.0 Added the $include_coalesce parameter.
*
* @param int $stackPtr The index of this token in the stack.
* @param bool $include_coalesce Optional. Whether or not to regard the null
* coalesce operator - ?? - as a comparison operator.
* Defaults to true.
* Null coalesce is a special comparison operator in this
* sense as it doesn't compare a variable to whatever is
* on the other side of the comparison operator.
*
* @return bool Whether this is a comparison.
*/
protected function is_comparison( $stackPtr, $include_coalesce = true ) {
$comparisonTokens = Tokens::$comparisonTokens;
if ( false === $include_coalesce ) {
unset( $comparisonTokens[ \T_COALESCE ] );
}
// 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( $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 ( false !== $next_token && \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 ( false !== $next_token && isset( $comparisonTokens[ $this->tokens[ $next_token ]['code'] ] ) ) {
return true;
}
return false;
}
/**
* Check if a token is inside of an array-value comparison function.
*
* @since 2.1.0
*
* @param int $stackPtr The index of the token in the stack.
*
* @return bool Whether the token is (part of) a parameter to an
* array-value comparison function.
*/
protected function is_in_array_comparison( $stackPtr ) {
$function_ptr = $this->is_in_function_call( $stackPtr, $this->arrayCompareFunctions, true, true );
if ( false === $function_ptr ) {
return false;
}
$function_name = $this->tokens[ $function_ptr ]['content'];
if ( true === $this->arrayCompareFunctions[ $function_name ] ) {
return true;
}
if ( $this->get_function_call_parameter_count( $function_ptr ) >= $this->arrayCompareFunctions[ $function_name ] ) {
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;
$closer = $this->tokens[ $stackPtr ]['bracket_closer'];
}
if ( isset( $opener, $closer ) ) {
return array(
'opener' => $opener,
'closer' => $closer,
);
}
return false;
}
/**
* Find the list opener & closer based on a T_LIST or T_OPEN_SHORT_ARRAY token.
*
* @since 2.2.0
*
* @param int $stackPtr The stack pointer to the array token.
*
* @return array|bool Array with two keys `opener`, `closer` or false if
* not a (short) list token or if either or these
* could not be determined.
*/
protected function find_list_open_close( $stackPtr ) {
/*
* Determine the list opener & closer.
*/
if ( \T_LIST === $this->tokens[ $stackPtr ]['code'] ) {
// PHPCS 3.5.0.
if ( isset( $this->tokens[ $stackPtr ]['parenthesis_opener'] ) ) {
$opener = $this->tokens[ $stackPtr ]['parenthesis_opener'];
} else {
// PHPCS < 3.5.0.
$next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( false !== $next_non_empty
&& \T_OPEN_PARENTHESIS === $this->tokens[ $next_non_empty ]['code']
) {
$opener = $next_non_empty;
}
}
if ( isset( $opener, $this->tokens[ $opener ]['parenthesis_closer'] ) ) {
$closer = $this->tokens[ $opener ]['parenthesis_closer'];
}
}
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr ) === true
) {
$opener = $stackPtr;
$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 `AbstractClassRestrictionsSniff` to this class.
*
* @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.