diff --git a/.travis.yml b/.travis.yml index d640d5d071..bbd5417579 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,7 @@ cache: # Cache directory for more recent Composer versions. - $HOME/.cache/composer/files -language: - - php +language: php php: - 5.4 @@ -19,7 +18,8 @@ php: - 7.1 - 7.2 - 7.3 - - "7.4snapshot" + - 7.4 + - "nightly" env: # `master` is now 3.x. @@ -114,7 +114,7 @@ jobs: allow_failures: # Allow failures for unstable builds. - - php: "7.4snapshot" + - php: "nightly" before_install: # Speed up build time by disabling Xdebug. diff --git a/CHANGELOG.md b/CHANGELOG.md index 67166eafda..072f0ac71d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a _No documentation available about unreleased changes as of yet._ +## [2.2.1] - 2020-02-04 + +### Added +- Metrics to the `WordPress.Arrays.CommaAfterArrayItem` sniff. These can be displayed using `--report=info`. +- The `sanitize_hex_color()` and the `sanitize_hex_color_no_hash()` functions to the `escapingFunctions` list used by the `WordPress.Security.EscapeOutput` sniff. + +### Changed +- The recommended version of the suggested DealerDirect PHPCS Composer plugin is now `^0.6`. + +### Fixed +- `WordPress.PHP.NoSilencedErrors`: depending on the custom properties set, the metrics would be different. +- `WordPress.WhiteSpace.ControlStructureSpacing`: fixed undefined index notice for closures with `use`. +- `WordPress.WP.GlobalVariablesOverride`: fixed undefined offset notice when the `treat_files_as_scoped` property would be set to `true`. +- `WordPress.WP.I18n`: fixed a _Trying to access array offset on value of type null_ error when the sniff was run on PHP 7.4 and would encounter a translation function expecting singular and plural texts for which one of these arguments was missing. ## [2.2.0] - 2019-11-11 @@ -1148,6 +1162,7 @@ See the comparison for full list. Initial tagged release. [Unreleased]: https://github.com/WordPress/WordPress-Coding-Standards/compare/master...HEAD +[2.2.1]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2.2.0...2.2.1 [2.2.0]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2.1.1...2.2.0 [2.1.1]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2.1.0...2.1.1 [2.1.0]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2.0.0...2.1.0 diff --git a/README.md b/README.md index 0b9d6fdc8b..42eda804b6 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ When installing the WordPress Coding Standards as a dependency in a larger proje There are two actively maintained Composer plugins which can handle the registration of standards with PHP_CodeSniffer for you: * [composer-phpcodesniffer-standards-plugin](https://github.com/higidi/composer-phpcodesniffer-standards-plugin) -* [phpcodesniffer-composer-installer](https://github.com/DealerDirect/phpcodesniffer-composer-installer):"^0.5.0" +* [phpcodesniffer-composer-installer](https://github.com/DealerDirect/phpcodesniffer-composer-installer):"^0.6" It is strongly suggested to `require` one of these plugins in your project to handle the registration of external standards with PHPCS for you. diff --git a/WordPress/Sniff.php b/WordPress/Sniff.php index ba010dcef1..8c3528b2fa 100644 --- a/WordPress/Sniff.php +++ b/WordPress/Sniff.php @@ -132,40 +132,42 @@ abstract class Sniff implements PHPCS_Sniff { * @var array */ protected $escapingFunctions = array( - 'absint' => true, - 'esc_attr__' => true, - 'esc_attr_e' => true, - 'esc_attr_x' => true, - 'esc_attr' => true, - 'esc_html__' => true, - 'esc_html_e' => true, - 'esc_html_x' => true, - 'esc_html' => true, - 'esc_js' => true, - 'esc_sql' => true, - 'esc_textarea' => true, - 'esc_url_raw' => true, - 'esc_url' => true, - 'filter_input' => true, - 'filter_var' => true, - 'floatval' => true, - 'highlight_string' => true, - 'intval' => true, - 'json_encode' => true, - 'like_escape' => true, - 'number_format' => true, - 'rawurlencode' => true, - 'sanitize_html_class' => true, - 'sanitize_key' => true, - 'sanitize_user_field' => true, - 'tag_escape' => true, - 'urlencode_deep' => true, - 'urlencode' => true, - 'wp_json_encode' => true, - 'wp_kses_allowed_html' => true, - 'wp_kses_data' => true, - 'wp_kses_post' => true, - 'wp_kses' => true, + '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, ); /** diff --git a/WordPress/Sniffs/Arrays/CommaAfterArrayItemSniff.php b/WordPress/Sniffs/Arrays/CommaAfterArrayItemSniff.php index 2a21950f23..902bc670b0 100644 --- a/WordPress/Sniffs/Arrays/CommaAfterArrayItemSniff.php +++ b/WordPress/Sniffs/Arrays/CommaAfterArrayItemSniff.php @@ -103,6 +103,12 @@ public function process_token( $stackPtr ) { */ if ( true === $single_line && $item_index === $array_item_count ) { + $this->phpcsFile->recordMetric( + $stackPtr, + 'Single line array - comma after last item', + ( true === $is_comma ? 'yes' : 'no' ) + ); + if ( true === $is_comma ) { $fix = $this->phpcsFile->addFixableError( 'Comma not allowed after last value in single-line array declaration', @@ -153,6 +159,14 @@ public function process_token( $stackPtr ) { } } + if ( false === $single_line && $item_index === $array_item_count ) { + $this->phpcsFile->recordMetric( + $stackPtr, + 'Multi-line array - comma after last item', + ( true === $is_comma ? 'yes' : 'no' ) + ); + } + if ( false === $is_comma ) { // Can't check spacing around the comma if there is no comma. continue; diff --git a/WordPress/Sniffs/PHP/NoSilencedErrorsSniff.php b/WordPress/Sniffs/PHP/NoSilencedErrorsSniff.php index 6e0dba4679..ee3b3b3a51 100644 --- a/WordPress/Sniffs/PHP/NoSilencedErrorsSniff.php +++ b/WordPress/Sniffs/PHP/NoSilencedErrorsSniff.php @@ -179,22 +179,24 @@ public function process_token( $stackPtr ) { $this->custom_whitelist = $this->merge_custom_array( $this->custom_whitelist, array(), false ); $this->custom_whitelist = array_map( 'strtolower', $this->custom_whitelist ); - if ( true === $this->use_default_whitelist || ! empty( $this->custom_whitelist ) ) { - /* - * Check if the error silencing is done for one of the whitelisted functions. - */ - $next_non_empty = $this->phpcsFile->findNext( $this->empty_tokens, ( $stackPtr + 1 ), null, true, null, true ); - if ( false !== $next_non_empty && \T_STRING === $this->tokens[ $next_non_empty ]['code'] ) { - $has_parenthesis = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true ); - if ( false !== $has_parenthesis && \T_OPEN_PARENTHESIS === $this->tokens[ $has_parenthesis ]['code'] ) { - $function_name = strtolower( $this->tokens[ $next_non_empty ]['content'] ); - if ( ( true === $this->use_default_whitelist - && isset( $this->function_whitelist[ $function_name ] ) === true ) - || in_array( $function_name, $this->custom_whitelist, true ) === true - ) { - $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', 'whitelisted function call: ' . $function_name ); - return; - } + /* + * Check if the error silencing is done for one of the whitelisted functions. + * + * @internal The function call name determination is done even when there is no whitelist active + * to allow the metrics to be more informative. + */ + $next_non_empty = $this->phpcsFile->findNext( $this->empty_tokens, ( $stackPtr + 1 ), null, true, null, true ); + if ( false !== $next_non_empty && \T_STRING === $this->tokens[ $next_non_empty ]['code'] ) { + $has_parenthesis = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true ); + if ( false !== $has_parenthesis && \T_OPEN_PARENTHESIS === $this->tokens[ $has_parenthesis ]['code'] ) { + $function_name = strtolower( $this->tokens[ $next_non_empty ]['content'] ); + if ( ( true === $this->use_default_whitelist + && isset( $this->function_whitelist[ $function_name ] ) === true ) + || ( ! empty( $this->custom_whitelist ) + && in_array( $function_name, $this->custom_whitelist, true ) === true ) + ) { + $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', 'whitelisted function call: ' . $function_name ); + return; } } } @@ -228,7 +230,7 @@ public function process_token( $stackPtr ) { ); if ( isset( $function_name ) ) { - $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', $function_name ); + $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', '@' . $function_name ); } else { $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', $found ); } diff --git a/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php b/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php index f81fb0fad5..37bf68b2a7 100644 --- a/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php +++ b/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php @@ -351,7 +351,7 @@ protected function process_global_statement( $stackPtr, $in_function_scope ) { $end = $this->tokens[ $scope_cond ]['scope_closer']; } else { // Global statement in the global namespace with file is being treated as scoped. - $end = ( $this->phpcsFile->numTokens + 1 ); + $end = $this->phpcsFile->numTokens; } for ( $ptr = $start; $ptr < $end; $ptr++ ) { diff --git a/WordPress/Sniffs/WP/I18nSniff.php b/WordPress/Sniffs/WP/I18nSniff.php index fdab5fad8e..d481955368 100644 --- a/WordPress/Sniffs/WP/I18nSniff.php +++ b/WordPress/Sniffs/WP/I18nSniff.php @@ -390,8 +390,16 @@ public function process_matched_token( $stack_ptr, $group_name, $matched_content $this->check_argument_tokens( $argument_assertion_context ); } - // For _n*() calls, compare the singular and plural strings. - if ( false !== strpos( $this->i18n_functions[ $matched_content ], 'number' ) ) { + /* + * For _n*() calls, compare the singular and plural strings. + * If either of the arguments is missing, empty or has more than 1 token, skip out. + * An error for that will already have been reported via the `check_argument_tokens()` method. + */ + if ( false !== strpos( $this->i18n_functions[ $matched_content ], 'number' ) + && isset( $argument_assertions[0]['tokens'], $argument_assertions[1]['tokens'] ) + && count( $argument_assertions[0]['tokens'] ) === 1 + && count( $argument_assertions[1]['tokens'] ) === 1 + ) { $single_context = $argument_assertions[0]; $plural_context = $argument_assertions[1]; diff --git a/WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php b/WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php index 29e9240057..5f7b9a3ca7 100644 --- a/WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php +++ b/WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php @@ -346,7 +346,8 @@ public function process_token( $stackPtr ) { if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisCloser + 1 ) ]['code'] && ! ( // Do NOT flag : immediately following ) for return types declarations. \T_COLON === $this->tokens[ ( $parenthesisCloser + 1 ) ]['code'] - && in_array( $this->tokens[ $this->tokens[ $parenthesisCloser ]['parenthesis_owner'] ]['code'], array( \T_FUNCTION, \T_CLOSURE ), true ) + && ( isset( $this->tokens[ $parenthesisCloser ]['parenthesis_owner'] ) === false + || in_array( $this->tokens[ $this->tokens[ $parenthesisCloser ]['parenthesis_owner'] ]['code'], array( \T_FUNCTION, \T_CLOSURE ), true ) ) ) && ( isset( $scopeOpener ) && \T_COLON !== $this->tokens[ $scopeOpener ]['code'] ) ) { diff --git a/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.6.inc b/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.6.inc new file mode 100644 index 0000000000..e9d4f2935a --- /dev/null +++ b/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.6.inc @@ -0,0 +1,7 @@ + 1, 157 => 1, 178 => 1, + 181 => 3, + 184 => 1, ); case 'I18nUnitTest.2.inc': diff --git a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc index 20d16585e0..6e848342a1 100644 --- a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc +++ b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc @@ -290,3 +290,18 @@ $returntype = function ( string $input ): string {}; // Ok. $returntype = function ( string $input ) : string {}; // Ok. $returntype = function ( string $input, array $inputs ): string {}; // Ok. $returntype = function ( string $input, array $inputs ) : string {}; // Ok. + +// Issue 1792. +$matching_options = array_filter( + $imported_options, + function ( $option ) use ( $option_id ): bool { + return $option['id'] === $option_id; + } +); + +$matching_options = array_filter( + $imported_options, + function ( $option ) use ( $option_id ) : bool { + return $option['id'] === $option_id; + } +); diff --git a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc.fixed b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc.fixed index 94e7256ca9..32c01f655d 100644 --- a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc.fixed +++ b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc.fixed @@ -279,3 +279,18 @@ $returntype = function ( string $input ): string {}; // Ok. $returntype = function ( string $input ) : string {}; // Ok. $returntype = function ( string $input, array $inputs ): string {}; // Ok. $returntype = function ( string $input, array $inputs ) : string {}; // Ok. + +// Issue 1792. +$matching_options = array_filter( + $imported_options, + function ( $option ) use ( $option_id ): bool { + return $option['id'] === $option_id; + } +); + +$matching_options = array_filter( + $imported_options, + function ( $option ) use ( $option_id ) : bool { + return $option['id'] === $option_id; + } +); diff --git a/composer.json b/composer.json index 8fd557c301..13d4afbc3d 100644 --- a/composer.json +++ b/composer.json @@ -19,12 +19,12 @@ "squizlabs/php_codesniffer": "^3.3.1" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", "phpcompatibility/php-compatibility": "^9.0", "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." }, "minimum-stability": "RC", "scripts": {