Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 58 additions & 69 deletions WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<int|string, int|string>
*/
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();
}

Expand Down Expand Up @@ -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).
*
Expand All @@ -202,67 +182,76 @@ 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.
if ( false === $has_non_false ) {
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;
}
}
86 changes: 59 additions & 27 deletions WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
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.
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 1, true ); // OK.
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.
Expand Down Expand Up @@ -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',
Expand All @@ -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.

Expand All @@ -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' ), <<<EOD
$version
EOD
, true ); // OK - contains interpolation, so the value can't be determined.
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<"EOD"
EOD
. '.0', true ); // OK - the heredoc is part of a larger expression.
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<'EOD'
EOD
, true ); // Error - a falsy value is not allowed for the $ver parameter.
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<EOD
0
EOD
, true ); // Error - a falsy value is not allowed for the $ver parameter.

// Safeguard handling of a double-quoted string passed as the $ver parameter.
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), "", true ); // Error - a falsy value is not allowed for the $ver parameter.
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), "$ver", true ); // Ok.
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), "${0}", true ); // Ok.
11 changes: 5 additions & 6 deletions WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,15 @@ public function getErrorList( $testFile = '' ) {
13 => 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':
Expand Down
Loading