diff --git a/WordPress-Extra/ruleset.xml b/WordPress-Extra/ruleset.xml index 370dc23a5f..6260d4fabb 100644 --- a/WordPress-Extra/ruleset.xml +++ b/WordPress-Extra/ruleset.xml @@ -68,6 +68,7 @@ + diff --git a/WordPress-VIP/ruleset.xml b/WordPress-VIP/ruleset.xml index 23c386b6a6..33b75e585b 100644 --- a/WordPress-VIP/ruleset.xml +++ b/WordPress-VIP/ruleset.xml @@ -88,4 +88,8 @@ %s() is highly discouraged, please use vip_safe_wp_remote_get() instead. + + + + diff --git a/WordPress/Sniff.php b/WordPress/Sniff.php index 70f763ac9e..a397ec60bd 100644 --- a/WordPress/Sniff.php +++ b/WordPress/Sniff.php @@ -2220,4 +2220,62 @@ public function has_html_open_tag( $tag_name, $stackPtr = null, $content = false return false; } + /** + * Check whether a T_CONST token is a class constant declaration. + * + * @since 0.14.0 + * + * @param int $stackPtr The position in the stack of the T_CONST token to verify. + * + * @return bool + */ + public function is_class_constant( $stackPtr ) { + if ( ! isset( $this->tokens[ $stackPtr ] ) || T_CONST !== $this->tokens[ $stackPtr ]['code'] ) { + return false; + } + + // Note: traits can not declare constants. + $valid_scopes = array( + 'T_CLASS' => true, + 'T_ANON_CLASS' => true, + 'T_INTERFACE' => true, + ); + + return $this->valid_direct_scope( $stackPtr, $valid_scopes ); + } + + /** + * 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 bool + */ + 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; + } + + return isset( $valid_scopes[ $this->tokens[ $ptr ]['type'] ] ); + } + } diff --git a/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php b/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php new file mode 100644 index 0000000000..7e47b042ea --- /dev/null +++ b/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php @@ -0,0 +1,221 @@ + 'get_stylesheet_directory()', + 'TEMPLATEPATH' => 'get_template_directory()', + 'PLUGINDIR' => 'WP_PLUGIN_DIR', + 'MUPLUGINDIR' => 'WPMU_PLUGIN_DIR', + 'HEADER_IMAGE' => 'add_theme_support( \'custom-header\' )', + 'NO_HEADER_TEXT' => 'add_theme_support( \'custom-header\' )', + 'HEADER_TEXTCOLOR' => 'add_theme_support( \'custom-header\' )', + 'HEADER_IMAGE_WIDTH' => 'add_theme_support( \'custom-header\' )', + 'HEADER_IMAGE_HEIGHT' => 'add_theme_support( \'custom-header\' )', + 'BACKGROUND_COLOR' => 'add_theme_support( \'custom-background\' )', + 'BACKGROUND_IMAGE' => 'add_theme_support( \'custom-background\' )', + ); + + /** + * Array of functions to check. + * + * @since 0.14.0 + * + * @var array => + */ + protected $target_functions = array( + 'define' => 1, + ); + + /** + * Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a constant. + * + * @var array + */ + private $preceding_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_GOTO => true, + + ); + + /** + * Processes this test, when one of its tokens is encountered. + * + * @since 0.14.0 + * + * @param int $stackPtr The position of the current token in the stack. + * + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. + */ + public function process_token( $stackPtr ) { + if ( isset( $this->target_functions[ strtolower( $this->tokens[ $stackPtr ]['content'] ) ] ) ) { + // Disallow excluding function groups for this sniff. + $this->exclude = ''; + + return parent::process_token( $stackPtr ); + + } else { + return $this->process_arbitrary_tstring( $stackPtr ); + } + } + + /** + * Process an arbitrary T_STRING token to determine whether it is one of the target constants. + * + * @since 0.14.0 + * + * @param int $stackPtr The position of the current token in the stack. + * + * @return void + */ + public function process_arbitrary_tstring( $stackPtr ) { + $content = $this->tokens[ $stackPtr ]['content']; + + if ( ! isset( $this->discouraged_constants[ $content ] ) ) { + return; + } + + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( false !== $next && T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] ) { + // Function call or declaration. + return; + } + + $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + if ( false !== $prev && isset( $this->preceding_tokens_to_ignore[ $this->tokens[ $prev ]['code'] ] ) ) { + // Not the use of a constant. + return; + } + + if ( false !== $prev + && T_NS_SEPARATOR === $this->tokens[ $prev ]['code'] + && T_STRING === $this->tokens[ ( $prev - 1 ) ]['code'] + ) { + // Namespaced constant of the same name. + return; + } + + if ( false !== $prev + && T_CONST === $this->tokens[ $prev ]['code'] + && true === $this->is_class_constant( $prev ) + ) { + // Class constant of the same name. + return; + } + + /* + * 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; + } + } + + $first_on_line = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); + if ( false !== $first_on_line && T_USE === $this->tokens[ $first_on_line ]['code'] ) { + $next_on_line = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $first_on_line + 1 ), null, true ); + if ( false !== $next_on_line ) { + if ( ( T_STRING === $this->tokens[ $next_on_line ]['code'] + && 'const' === $this->tokens[ $next_on_line ]['content'] ) + || T_CONST === $this->tokens[ $next_on_line ]['code'] // Happens in some PHPCS versions. + ) { + $has_ns_sep = $this->phpcsFile->findNext( T_NS_SEPARATOR, ( $next_on_line + 1 ), $stackPtr ); + if ( false !== $has_ns_sep ) { + // Namespaced const (group) use statement. + return; + } + } else { + // Not a const use statement. + return; + } + } + } + + // Ok, this is really one of the discouraged constants. + $this->phpcsFile->addWarning( + 'Found usage of constant "%s". Use %s instead.', + $stackPtr, + 'UsageFound', + array( + $content, + $this->discouraged_constants[ $content ], + ) + ); + } + + /** + * Process the parameters of a matched `define` function call. + * + * @since 0.14.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param array $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $function_name = strtolower( $matched_content ); + $target_param = $this->target_functions[ $function_name ]; + + // Was the target parameter passed ? + if ( ! isset( $parameters[ $target_param ] ) ) { + return; + } + + $raw_content = $this->strip_quotes( $parameters[ $target_param ]['raw'] ); + + if ( isset( $this->discouraged_constants[ $raw_content ] ) ) { + $this->phpcsFile->addWarning( + 'Found declaration of constant "%s". Use %s instead.', + $stackPtr, + 'DeclarationFound', + array( + $raw_content, + $this->discouraged_constants[ $raw_content ], + ) + ); + } + } + +} // End class. diff --git a/WordPress/Tests/WP/DiscouragedConstantsUnitTest.inc b/WordPress/Tests/WP/DiscouragedConstantsUnitTest.inc new file mode 100644 index 0000000000..687705e7fc --- /dev/null +++ b/WordPress/Tests/WP/DiscouragedConstantsUnitTest.inc @@ -0,0 +1,72 @@ +STYLESHEETPATH; +use const SomeNamespace\STYLESHEETPATH as SSP; // PHP 5.6+ +use const SomeNamespace\{STYLESHEETPATH, TEMPLATEPATH}; // PHP 7.0+ +define( 'My\STYLESHEETPATH', 'something' ); + +// Ok, not usage of the constant as such. +if ( defined( 'STYLESHEETPATH' ) ) { // Ok. + // Do something unrelated. +} + + +/* + * These are all bad. + */ +echo STYLESHEETPATH; +echo \STYLESHEETPATH; // Global constant. +$folder = basename( TEMPLATEPATH ); +include PLUGINDIR . '/js/myfile.js'; +echo MUPLUGINDIR; +echo HEADER_IMAGE; +echo NO_HEADER_TEXT; +echo HEADER_TEXTCOLOR; +echo HEADER_IMAGE_WIDTH; +echo HEADER_IMAGE_HEIGHT; +echo BACKGROUND_COLOR; +echo BACKGROUND_IMAGE; + +use const STYLESHEETPATH as SSP; +use const ABC as STYLESHEETPATH; + +switch( STYLESHEETPATH ) { + case STYLESHEETPATH: + break; +} + +define( 'STYLESHEETPATH', 'something' ); +const STYLESHEETPATH = 'something'; diff --git a/WordPress/Tests/WP/DiscouragedConstantsUnitTest.php b/WordPress/Tests/WP/DiscouragedConstantsUnitTest.php new file mode 100644 index 0000000000..3e60ba4bc2 --- /dev/null +++ b/WordPress/Tests/WP/DiscouragedConstantsUnitTest.php @@ -0,0 +1,59 @@ + => + */ + public function getErrorList() { + return array(); + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() { + return array( + 50 => 1, + 51 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 63 => 1, + 64 => 1, + 66 => 1, + 67 => 1, + 71 => 1, + 72 => 1, + ); + } + +} // End class.