diff --git a/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php b/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php index 28b4ba8d6b..f821da1751 100644 --- a/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php +++ b/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php @@ -10,8 +10,11 @@ namespace WordPressCS\WordPress\Sniffs\WP; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; use PHPCSUtils\Utils\Numbers; use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -65,46 +68,17 @@ final class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSni \T_NS_SEPARATOR => \T_NS_SEPARATOR, // Needed to handle fully qualified \false (PHPCS 3.x). ); - /** - * Token codes which are "safe" to accept to determine whether a version would evaluate to `false`. - * - * This array is enriched with several of the PHPCS token arrays in the register() method. - * - * @var array - */ - private $safe_tokens = array( - \T_NULL => \T_NULL, - \T_FALSE => \T_FALSE, - \T_TRUE => \T_TRUE, - \T_LNUMBER => \T_LNUMBER, - \T_DNUMBER => \T_DNUMBER, - \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, - \T_START_NOWDOC => \T_START_NOWDOC, - \T_NOWDOC => \T_NOWDOC, - \T_END_NOWDOC => \T_END_NOWDOC, - \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS, - \T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS, - \T_STRING_CONCAT => \T_STRING_CONCAT, - ); - /** * Returns an array of tokens this test wants to listen for. * * Overloads and calls the parent method to allow for adding additional tokens to the - * $false_tokens and $safe_tokens properties. + * $false_tokens property. * * @return array */ public function register() { $this->false_tokens += Tokens::$emptyTokens; - $this->safe_tokens += Tokens::$emptyTokens; - $this->safe_tokens += Tokens::$assignmentTokens; - $this->safe_tokens += Tokens::$comparisonTokens; - $this->safe_tokens += Tokens::$operators; - $this->safe_tokens += Tokens::$booleanOperators; - $this->safe_tokens += Tokens::$castTokens; - return parent::register(); } @@ -194,6 +168,12 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p /** * Determine if a range has a falsy value. * + * Only a limited set of values is recognized as falsy: + * - Boolean false. + * - An integer or float equal to zero. + * - A text string with the content `'0'` or `''` (single or double-quoted, heredoc, or nowdoc). + * - An empty array. + * * @param int $start The position to start looking from. * @param int $end The position to stop looking (inclusive). * @@ -202,7 +182,6 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p * couldn't be reliably determined. */ protected function is_falsy( $start, $end ) { - // Find anything excluding the false tokens. $has_non_false = $this->phpcsFile->findNext( $this->false_tokens, $start, ( $end + 1 ), true ); // If no non-false tokens are found, we are good. @@ -210,59 +189,69 @@ protected function is_falsy( $start, $end ) { return true; } - $code_string = ''; - for ( $i = $start; $i <= $end; $i++ ) { - if ( isset( $this->safe_tokens[ $this->tokens[ $i ]['code'] ] ) === false ) { - // Function call/variable or other token which makes it neigh impossible - // to determine whether the actual value would evaluate to false. + $target_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $start, ( $end + 1 ), true ); + + // An array only evaluates to false when it is empty. + if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $target_ptr ]['code'] ] ) ) { + $open_close = Arrays::getOpenClose( $this->phpcsFile, $target_ptr ); + if ( false === $open_close ) { + // Short list assignment, not an array. return false; } - if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { - continue; - } + // Bail if there is any non-empty token in the $ver parameter after the array, as that's a more complex + // expression which can't be reliably evaluated. + $next_after_array = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + ( $open_close['closer'] + 1 ), + ( $end + 1 ), + true + ); - // Make sure that PHP 7.4 numeric literals and PHP 8.1 explicit octals don't cause problems. - if ( \T_LNUMBER === $this->tokens[ $i ]['code'] || \T_DNUMBER === $this->tokens[ $i ]['code'] ) { - $number_info = Numbers::getCompleteNumber( $this->phpcsFile, $i ); - $code_string .= $number_info['decimal']; - $i = $number_info['last_token']; - continue; + if ( false !== $next_after_array ) { + return false; } - // Make sure that when deprecated casts are used in the code under scan and the sniff is run on PHP 8.5, - // the eval() won't cause a deprecation notice, borking the scan of the file. - if ( \PHP_VERSION_ID >= 80500 ) { - if ( \T_INT_CAST === $this->tokens[ $i ]['code'] ) { - $code_string .= '(int)'; - continue; - } + $first_non_empty_in_array = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + ( $open_close['opener'] + 1 ), + $open_close['closer'], + true + ); - if ( \T_DOUBLE_CAST === $this->tokens[ $i ]['code'] ) { - $code_string .= '(float)'; - continue; - } + return ( false === $first_non_empty_in_array ); + } - if ( \T_BOOL_CAST === $this->tokens[ $i ]['code'] ) { - $code_string .= '(bool)'; - continue; - } + // Check if it is a '0' or '' string. + if ( isset( Collections::textStringStartTokens()[ $this->tokens[ $target_ptr ]['code'] ] ) ) { + if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $target_ptr ]['code'] ) { + // No need to examine as it will never match/can't be determined. + return false; + } - if ( \T_BINARY_CAST === $this->tokens[ $i ]['code'] ) { - $code_string .= '(string)'; - continue; - } + $valid_tokens = array( \T_CONSTANT_ENCAPSED_STRING ) + Tokens::$heredocTokens + Tokens::$emptyTokens; + if ( false !== $this->phpcsFile->findNext( $valid_tokens, $start, ( $end + 1 ), true ) ) { + // Bail if the $ver parameter is more than a single text string. + return false; } - $code_string .= $this->tokens[ $i ]['content']; + $content = TextStrings::getCompleteTextString( $this->phpcsFile, $target_ptr ); + + return '0' === $content || '' === $content; } - if ( '' === $code_string ) { + // The int/float check below only handles a single literal token, so bail if there is more than one non-empty token. + if ( false !== $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $target_ptr + 1 ), ( $end + 1 ), true ) ) { return false; } - // Evaluate the argument to figure out the outcome is false or not. - // phpcs:ignore Squiz.PHP.Eval -- No harm here. - return ( false === eval( "return (bool) $code_string;" ) ); + // Check if it is an int or float equal to zero. + if ( \T_LNUMBER === $this->tokens[ $target_ptr ]['code'] || \T_DNUMBER === $this->tokens[ $target_ptr ]['code'] ) { + $number_info = Numbers::getCompleteNumber( $this->phpcsFile, $target_ptr ); + + return 0.0 === (float) $number_info['decimal']; + } + + return false; } } diff --git a/WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc b/WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc index 8592a34121..f1bb016792 100644 --- a/WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc +++ b/WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc @@ -3,15 +3,15 @@ wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ) ); // Warning - missing $ver, Warning - In Footer is set to a falsy (default) value. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), /* comment */ '1.1.1', true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), /* comment */ '0' /* another comment */, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), /* comment */ '0' /* another comment */, true ); // Error - a falsy value is not allowed for the $ver parameter. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '1.1.1', $in_footer ); // OK, the value is set. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), false, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), null, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0.0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 00.00, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0x0, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0, true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), false, true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), null, true ); // Warning - $ver is NULL. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0.0, true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 00.00, true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0x0, true ); // Error - a falsy value is not allowed for the $ver parameter. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 100.001, true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0x1, true ); // Hex number, OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 052, true ); // Octal number, OK. @@ -19,7 +19,7 @@ wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0.1, true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 1.0, true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 2 * 8, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0', true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0', true ); // Error - a falsy value is not allowed for the $ver parameter. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0.0.0', true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0.1.0', true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0' . '0', true ); // OK. @@ -51,14 +51,14 @@ wp_register_style( 'someScript-js' ); // OK. wp_enqueue_style( 'someScript-js' ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (bool) 1, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (bool) 0, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (bool) 0, true ); // OK - a falsy value behind a cast is not flagged. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (binary) 123, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), b'0', true ); // Error - 0, false or NULL are not allowed. +// Safeguard handling of arithmetic operations. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0 + 1, true ); // OK. // Safeguard support for PHP 8.0+ named parameters. wp_register_script( - ver : 0, // Error - 0, false or NULL are not allowed. + ver : 0, // Error - a falsy value is not allowed for the $ver parameter. in_footer: false, src : 'https://example.com/someScript.js', handle : 'someScript-js', @@ -73,39 +73,39 @@ wp_register_script( 'someScript-js', 'https://example.com/someScript.js', array( 'jquery' ), - // Error - 0, false or NULL are not allowed. + // Warning - $ver is NULL. null, true, ); // Safeguard handling of PHP 7.4 numeric literals with underscores. -wp_register_script( 'someScript-js', $url, [], 0_0.0_0, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', $url, [], 0_0.0_0, true ); // Error - a falsy value is not allowed for the $ver parameter. // Safeguard handling of PHP 8.1 explicit octals. -wp_register_script( 'someScript-js', $url, [], 0o0, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', $url, [], 0o0, true ); // Error - a falsy value is not allowed for the $ver parameter. + + + + + + + + -// Safeguard against PHP 8.5 deprecation of non-standard cast names. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (boolean) 1, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (boolean) 0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (integer) 1, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (integer) 0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (double) 1, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (double) 0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (binary) 0, true ); // Error - 0, false or NULL are not allowed. // Safeguard handling of non-lowercase `null`. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), NULL, true ); // Warning - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), NULL, true ); // Warning - $ver is NULL. /* * Safeguard handling of fully qualified \true, \false and \null. * Also safeguard that adding T_NS_SEPARATOR to $false_tokens doesn't cause false positives due to problems in is_falsy(). */ -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \FALSE, \true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \null, \TRUE ); // Warning - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \Null, true ); // Warning - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \FALSE, \true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \null, \TRUE ); // Warning - $ver is NULL. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \Null, true ); // Warning - $ver is NULL. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \true, \False ); // Ok. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \get_version(), \null ); // OK. @@ -118,3 +118,35 @@ MyNamespace\wp_register_style( 'style-name', 'https://example.com/style.css' ); \MyNamespace\wp_enqueue_style( 'style-name', 'https://example.com/style.css' ); // Ok. namespace\wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '1.1.0' ); // The sniff should start flagging this once it can resolve relative namespaces (once it does, it should be "Warning - missing $in_footer"). namespace\Sub\wp_enqueue_style( 'style-name', 'https://example.com/style.css' ); // Ok. + +// Safeguard handling of an array passed as the $ver parameter. Only an empty array evaluates to false. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [ '1.0.0' ], true ); // OK. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [] + [ 1 ], true ); // OK - the array is part of a larger expression. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [ $a ] = array( 1, 2, 3 ), true ); // OK - short list assignment, not an array. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), array(), true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [ /* comment */ ], true ); // Error - a falsy value is not allowed for the $ver parameter. + +// Safeguard handling of a heredoc/nowdoc passed as the $ver parameter. Only an empty or "0" body evaluates to false. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<'EOD' +1.0.0 +EOD +, true ); // OK. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), << 1, 14 => 1, 22 => 1, - 54 => 1, - 57 => 1, 61 => 1, 82 => 1, 85 => 1, - 89 => 1, - 92 => 1, - 95 => 1, - 97 => 1, 106 => 1, + 126 => 1, + 127 => 1, + 141 => 1, + 144 => 1, + 150 => 1, ); case 'EnqueuedResourceParametersUnitTest.2.inc':