3424 lines
111 KiB
PHP
3424 lines
111 KiB
PHP
<?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.1';
|
|
|
|
/**
|
|
* 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_hex_color' => true,
|
|
'sanitize_hex_color_no_hash' => 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.
|
|
*
|
|
* @since 0.12.0 A lesser variant of this method previously existed in the
|
|
* `AbstractClassRestrictionsSniff` class.
|
|
*
|
|
* @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 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 ( $this->is_token_namespaced( $stackPtr ) === true ) {
|
|
// 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 );
|
|
}
|
|
|
|
/**
|
|
* Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a short list() construct.
|
|
*
|
|
* @internal This function will be introduced in PHPCS upstream in version 3.5.0
|
|
* and can be removed from WPCS once WPCS raises the minimum version.
|
|
*
|
|
* @since 2.2.0
|
|
*
|
|
* @param int $stackPtr The position of the array bracket token.
|
|
*
|
|
* @return bool True if the token passed is the open/close bracket of a short list.
|
|
* False if the token is a short array bracket or not
|
|
* a T_OPEN/CLOSE_SHORT_ARRAY token.
|
|
*/
|
|
protected function is_short_list( $stackPtr ) {
|
|
// Is this one of the tokens this function handles ?
|
|
if ( \T_OPEN_SHORT_ARRAY !== $this->tokens[ $stackPtr ]['code']
|
|
&& \T_CLOSE_SHORT_ARRAY !== $this->tokens[ $stackPtr ]['code']
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
switch ( $this->tokens[ $stackPtr ]['code'] ) {
|
|
case \T_OPEN_SHORT_ARRAY:
|
|
$opener = $stackPtr;
|
|
$closer = $this->tokens[ $stackPtr ]['bracket_closer'];
|
|
break;
|
|
|
|
case \T_CLOSE_SHORT_ARRAY:
|
|
$opener = $this->tokens[ $stackPtr ]['bracket_opener'];
|
|
$closer = $stackPtr;
|
|
break;
|
|
}
|
|
|
|
$nextNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $closer + 1 ), null, true, null, true );
|
|
if ( false !== $nextNonEmpty && \T_EQUAL === $this->tokens[ $nextNonEmpty ]['code'] ) {
|
|
return true;
|
|
}
|
|
|
|
// Check for short list in foreach, i.e. `foreach($array as [$a, $b])`.
|
|
if ( $this->is_foreach_as( $stackPtr ) === true ) {
|
|
return true;
|
|
}
|
|
|
|
// Maybe this is a short list syntax nested inside another short list syntax ?
|
|
$parentOpen = $opener;
|
|
do {
|
|
$parentOpen = $this->phpcsFile->findPrevious(
|
|
\T_OPEN_SHORT_ARRAY,
|
|
( $parentOpen - 1 ),
|
|
null,
|
|
false,
|
|
null,
|
|
true
|
|
);
|
|
|
|
if ( false === $parentOpen ) {
|
|
return false;
|
|
}
|
|
} while ( $this->tokens[ $parentOpen ]['bracket_closer'] < $opener );
|
|
|
|
return $this->is_short_list( $parentOpen );
|
|
}
|
|
|
|
/**
|
|
* Get a list of the token pointers to the variables being assigned to in a list statement.
|
|
*
|
|
* @internal No need to take special measures for nested lists. Nested or not,
|
|
* each list part can only contain one variable being written to.
|
|
*
|
|
* @since 2.2.0
|
|
*
|
|
* @param int $stackPtr The position of the T_LIST or T_OPEN_SHORT_ARRAY
|
|
* token in the stack.
|
|
* @param array $list_open_close Optional. Array containing the token pointers to
|
|
* the list opener and closer.
|
|
*
|
|
* @return array Array with the stack pointers to the variables or an empty
|
|
* array when not a (short) list.
|
|
*/
|
|
protected function get_list_variables( $stackPtr, $list_open_close = array() ) {
|
|
if ( \T_LIST !== $this->tokens[ $stackPtr ]['code']
|
|
&& \T_OPEN_SHORT_ARRAY !== $this->tokens[ $stackPtr ]['code']
|
|
) {
|
|
return array();
|
|
}
|
|
|
|
if ( empty( $list_open_close ) ) {
|
|
$list_open_close = $this->find_list_open_close( $stackPtr );
|
|
if ( false === $list_open_close ) {
|
|
// Not a (short) list.
|
|
return array();
|
|
}
|
|
}
|
|
|
|
$var_pointers = array();
|
|
$current = $list_open_close['opener'];
|
|
$closer = $list_open_close['closer'];
|
|
$last = false;
|
|
do {
|
|
++$current;
|
|
$next_comma = $this->phpcsFile->findNext( \T_COMMA, $current, $closer );
|
|
if ( false === $next_comma ) {
|
|
$next_comma = $closer;
|
|
$last = true;
|
|
}
|
|
|
|
// Skip over the "key" part in keyed lists.
|
|
$arrow = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $current, $next_comma );
|
|
if ( false !== $arrow ) {
|
|
$current = ( $arrow + 1 );
|
|
}
|
|
|
|
/*
|
|
* Each list item can only have one variable to which an assignment is being made.
|
|
* This can be an array with a (variable) index, but that doesn't matter, we're only
|
|
* concerned with the actual variable.
|
|
*/
|
|
$var = $this->phpcsFile->findNext( \T_VARIABLE, $current, $next_comma );
|
|
if ( false !== $var ) {
|
|
// Not an empty list item.
|
|
$var_pointers[] = $var;
|
|
}
|
|
|
|
$current = $next_comma;
|
|
|
|
} while ( false === $last );
|
|
|
|
return $var_pointers;
|
|
}
|
|
|
|
/**
|
|
* Check whether a function has been marked as deprecated via a @deprecated tag
|
|
* in the function docblock.
|
|
*
|
|
* {@internal This method is static to allow the ValidFunctionName class to use it.}}
|
|
*
|
|
* @since 2.2.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position of a T_FUNCTION
|
|
* token in the stack.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function is_function_deprecated( File $phpcsFile, $stackPtr ) {
|
|
$tokens = $phpcsFile->getTokens();
|
|
$find = Tokens::$methodPrefixes;
|
|
$find[] = \T_WHITESPACE;
|
|
|
|
$comment_end = $phpcsFile->findPrevious( $find, ( $stackPtr - 1 ), null, true );
|
|
if ( \T_DOC_COMMENT_CLOSE_TAG !== $tokens[ $comment_end ]['code'] ) {
|
|
// Function doesn't have a doc comment or is using the wrong type of comment.
|
|
return false;
|
|
}
|
|
|
|
$comment_start = $tokens[ $comment_end ]['comment_opener'];
|
|
foreach ( $tokens[ $comment_start ]['comment_tags'] as $tag ) {
|
|
if ( '@deprecated' === $tokens[ $tag ]['content'] ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|