From e3eab49f12b6538d2c9054edc9eef35f14eb4ff1 Mon Sep 17 00:00:00 2001 From: Anton Vlasenko <43744263+anton-vlasenko@users.noreply.github.com> Date: Fri, 10 May 2024 18:02:21 +0200 Subject: [PATCH] Enforce @since tags in /packages/block-serialization-default-parser/ and other files (#60007) 1. Added support for checking `@since` tags for class, trait, interface, and enum declarations. 2. Added support for checking `@since` tags for class and trait properties; 3. Added support for checking `@since` tags for class, interface, and trait method declarations; 4. Added support for checking `@since` tags for WordPress functions that invoke hooks: `do_action()`, `do_action_ref_array()`, `apply_filters()`, `apply_filters_ref_array()`. 5. Refactored unit tests; duplicate code moved to the new `GutenbergCS\Gutenberg\Tests\AbstractSniffUnitTest` class. Co-authored-by: anton-vlasenko Co-authored-by: rodrigoprimo Co-authored-by: gziolo --- .../block-library/src/latest-posts/index.php | 1 + .../block-library/src/navigation/index.php | 52 +- .../src/blocks/legacy-widget/index.php | 6 + .../widgets/src/blocks/widget-group/index.php | 8 + phpcs.xml.dist | 5 +- .../FunctionCommentSinceTagSniff.php | 162 ----- .../Sniffs/Commenting/SinceTagSniff.php | 617 +++++++++++++++++ .../Gutenberg/Tests/AbstractSniffUnitTest.php | 85 +++ .../ForbiddenFunctionsAndClassesUnitTest.php | 56 +- .../GuardedFunctionAndClassNamesUnitTest.php | 56 +- .../FunctionCommentSinceTagUnitTest.inc | 39 -- .../FunctionCommentSinceTagUnitTest.php | 42 -- .../Tests/Commenting/SinceTagUnitTest.inc | 629 ++++++++++++++++++ .../Tests/Commenting/SinceTagUnitTest.php | 189 ++++++ .../ValidBlockLibraryFunctionNameUnitTest.php | 56 +- .../Gutenberg/ruleset.xml | 1 + .../gutenberg-coding-standards/composer.json | 3 +- 17 files changed, 1623 insertions(+), 384 deletions(-) delete mode 100644 test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/FunctionCommentSinceTagSniff.php create mode 100644 test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php create mode 100644 test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php delete mode 100644 test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.inc delete mode 100644 test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.php create mode 100644 test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.inc create mode 100644 test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 913a9ae2d430e..85c7b58737a1c 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -152,6 +152,7 @@ function render_block_core_latest_posts( $attributes ) { * […] is the default excerpt ending from wp_trim_excerpt() in Core. */ if ( str_ends_with( $trimmed_excerpt, ' […]' ) ) { + /** This filter is documented in wp-includes/formatting.php */ $excerpt_length = (int) apply_filters( 'excerpt_length', $block_core_latest_posts_excerpt_length ); if ( $excerpt_length <= $block_core_latest_posts_excerpt_length ) { $trimmed_excerpt = substr( $trimmed_excerpt, 0, -11 ); diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index ddb4e14606924..c5ef8e9204b32 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -7,17 +7,23 @@ /** * Helper functions used to render the navigation block. + * + * @since 6.5.0 */ class WP_Navigation_Block_Renderer { /** * Used to determine whether or not a navigation has submenus. + * + * @since 6.5.0 */ private static $has_submenus = false; /** * Used to determine which blocks need an
  • wrapper. * + * @since 6.5.0 + * * @var array */ private static $needs_list_item_wrapper = array( @@ -29,6 +35,8 @@ class WP_Navigation_Block_Renderer { /** * Keeps track of all the navigation names that have been seen. * + * @since 6.5.0 + * * @var array */ private static $seen_menu_names = array(); @@ -36,6 +44,8 @@ class WP_Navigation_Block_Renderer { /** * Returns whether or not this is responsive navigation. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @return bool Returns whether or not this is responsive navigation. */ @@ -51,6 +61,8 @@ private static function is_responsive( $attributes ) { /** * Returns whether or not a navigation has a submenu. * + * @since 6.5.0 + * * @param WP_Block_List $inner_blocks The list of inner blocks. * @return bool Returns whether or not a navigation has a submenu and also sets the member variable. */ @@ -88,6 +100,8 @@ private static function has_submenus( $inner_blocks ) { /** * Determine whether the navigation blocks is interactive. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @param WP_Block_List $inner_blocks The list of inner blocks. * @return bool Returns whether or not to load the view script. @@ -101,6 +115,8 @@ private static function is_interactive( $attributes, $inner_blocks ) { /** * Returns whether or not a block needs a list item wrapper. * + * @since 6.5.0 + * * @param WP_Block $block The block. * @return bool Returns whether or not a block needs a list item wrapper. */ @@ -127,6 +143,8 @@ private static function does_block_need_a_list_item_wrapper( $block ) { /** * Returns the markup for a single inner block. * + * @since 6.5.0 + * * @param WP_Block $inner_block The inner block. * @return string Returns the markup for a single inner block. */ @@ -144,6 +162,8 @@ private static function get_markup_for_inner_block( $inner_block ) { /** * Returns the html for the inner blocks of the navigation block. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @param WP_Block_List $inner_blocks The list of inner blocks. * @return string Returns the html for the inner blocks of the navigation block. @@ -201,6 +221,8 @@ private static function get_inner_blocks_html( $attributes, $inner_blocks ) { /** * Gets the inner blocks for the navigation block from the navigation post. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @return WP_Block_List Returns the inner blocks for the navigation block. */ @@ -236,6 +258,8 @@ private static function get_inner_blocks_from_navigation_post( $attributes ) { /** * Gets the inner blocks for the navigation block from the fallback. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @return WP_Block_List Returns the inner blocks for the navigation block. */ @@ -253,6 +277,8 @@ private static function get_inner_blocks_from_fallback( $attributes ) { /** * Gets the inner blocks for the navigation block. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @param WP_Block $block The parsed block. * @return WP_Block_List Returns the inner blocks for the navigation block. @@ -311,6 +337,8 @@ private static function get_inner_blocks( $attributes, $block ) { /** * Gets the name of the current navigation, if it has one. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @return string Returns the name of the navigation. */ @@ -346,6 +374,8 @@ private static function get_navigation_name( $attributes ) { /** * Returns the layout class for the navigation block. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @return string Returns the layout class for the navigation block. */ @@ -377,6 +407,8 @@ private static function get_layout_class( $attributes ) { /** * Return classes for the navigation block. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @return string Returns the classes for the navigation block. */ @@ -404,6 +436,8 @@ private static function get_classes( $attributes ) { /** * Get styles for the navigation block. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @return string Returns the styles for the navigation block. */ @@ -417,6 +451,8 @@ private static function get_styles( $attributes ) { /** * Get the responsive container markup * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @param WP_Block_List $inner_blocks The list of inner blocks. * @param string $inner_blocks_html The markup for the inner blocks. @@ -515,6 +551,8 @@ private static function get_responsive_container_markup( $attributes, $inner_blo /** * Get the wrapper attributes * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @param WP_Block_List $inner_blocks A list of inner blocks. * @return string Returns the navigation block markup. @@ -544,6 +582,8 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) /** * Gets the nav element directives. * + * @since 6.5.0 + * * @param bool $is_interactive Whether the block is interactive. * @return string the directives for the navigation element. */ @@ -574,6 +614,8 @@ private static function get_nav_element_directives( $is_interactive ) { /** * Handle view script module loading. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @param WP_Block $block The parsed block. * @param WP_Block_List $inner_blocks The list of inner blocks. @@ -598,6 +640,8 @@ private static function handle_view_script_module_loading( $attributes, $block, /** * Returns the markup for the navigation block. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @param WP_Block_List $inner_blocks The list of inner blocks. * @return string Returns the navigation wrapper markup. @@ -613,6 +657,8 @@ private static function get_wrapper_markup( $attributes, $inner_blocks ) { /** * Returns a unique name for the navigation. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @return string Returns a unique name for the navigation. */ @@ -632,6 +678,8 @@ private static function get_unique_navigation_name( $attributes ) { /** * Renders the navigation block. * + * @since 6.5.0 + * * @param array $attributes The block attributes. * @param string $content The saved content. * @param WP_Block $block The parsed block. @@ -1604,7 +1652,9 @@ function block_core_navigation_insert_hooked_blocks_into_rest_response( $respons // Remove mock Navigation block wrapper. $content = block_core_navigation_remove_serialized_parent_block( $content ); - $response->data['content']['raw'] = $content; + $response->data['content']['raw'] = $content; + + /** This filter is documented in wp-includes/post-template.php */ $response->data['content']['rendered'] = apply_filters( 'the_content', $content ); return $response; diff --git a/packages/widgets/src/blocks/legacy-widget/index.php b/packages/widgets/src/blocks/legacy-widget/index.php index 24ea288be3875..ee13ae9a1c612 100644 --- a/packages/widgets/src/blocks/legacy-widget/index.php +++ b/packages/widgets/src/blocks/legacy-widget/index.php @@ -8,6 +8,8 @@ /** * Renders the 'core/legacy-widget' block. * + * @since 5.8.0 + * * @global int $wp_widget_factory. * * @param array $attributes The block attributes. @@ -56,6 +58,8 @@ function render_block_core_legacy_widget( $attributes ) { /** * Registers the 'core/legacy-widget' block. + * + * @since 5.8.0 */ function register_block_core_legacy_widget() { register_block_type_from_metadata( @@ -72,6 +76,8 @@ function register_block_core_legacy_widget() { * Intercepts any request with legacy-widget-preview in the query param and, if * set, renders a page containing a preview of the requested Legacy Widget * block. + * + * @since 5.8.0 */ function handle_legacy_widget_preview_iframe() { if ( empty( $_GET['legacy-widget-preview'] ) ) { diff --git a/packages/widgets/src/blocks/widget-group/index.php b/packages/widgets/src/blocks/widget-group/index.php index 284ca66a85ce7..e8769612a2f17 100644 --- a/packages/widgets/src/blocks/widget-group/index.php +++ b/packages/widgets/src/blocks/widget-group/index.php @@ -8,6 +8,8 @@ /** * Renders the 'core/widget-group' block. * + * @since 5.9.0 + * * @global array $wp_registered_sidebars * @global int|string $_sidebar_being_rendered * @@ -45,6 +47,8 @@ function render_block_core_widget_group( $attributes, $content, $block ) { /** * Registers the 'core/widget-group' block. + * + * @since 5.9.0 */ function register_block_core_widget_group() { register_block_type_from_metadata( @@ -62,6 +66,8 @@ function register_block_core_widget_group() { * it. This lets us get to the current sidebar in * render_block_core_widget_group(). * + * @since 5.9.0 + * * @global int|string $_sidebar_being_rendered * * @param int|string $index Index, name, or ID of the dynamic sidebar. @@ -76,6 +82,8 @@ function note_sidebar_being_rendered( $index ) { * Clear whatever we set in note_sidebar_being_rendered() after WordPress * finishes rendering a sidebar. * + * @since 5.9.0 + * * @global int|string $_sidebar_being_rendered */ function discard_sidebar_being_rendered() { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 400b4d68ece44..45d742a498c65 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -172,8 +172,11 @@ - + /packages/block-library/src/.+/*\.php$ + /packages/block-serialization-default-parser/.+/*\.php$ + /packages/widgets/src/blocks/legacy-widget/index\.php$ + /packages/widgets/src/blocks/widget-group/index\.php$ diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/FunctionCommentSinceTagSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/FunctionCommentSinceTagSniff.php deleted file mode 100644 index 54b8b367560fc..0000000000000 --- a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/FunctionCommentSinceTagSniff.php +++ /dev/null @@ -1,162 +0,0 @@ - - */ - public function register() { - return array( T_FUNCTION ); - } - - /** - * Processes the tokens that this sniff is interested in. - * - * @param File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the stack passed in $tokens. - */ - public function process( File $phpcsFile, $stackPtr ) { - if ( static::is_experimental_block( $phpcsFile ) ) { - // The "@since" tag is not required for experimental blocks since they are not yet included in WordPress Core. - return; - } - - $tokens = $phpcsFile->getTokens(); - $function_name = $phpcsFile->getDeclarationName( $stackPtr ); - - $wrapping_tokens_to_check = array( - T_CLASS, - T_INTERFACE, - T_TRAIT, - ); - - foreach ( $wrapping_tokens_to_check as $wrapping_token_to_check ) { - if ( false !== $phpcsFile->getCondition( $stackPtr, $wrapping_token_to_check, false ) ) { - // This sniff only processes functions, not class methods. - return; - } - } - - $missing_since_tag_error_message = sprintf( '@since tag is missing for the "%s()" function.', $function_name ); - - // All these tokens could be present before the docblock. - $tokens_before_the_docblock = array( - T_PUBLIC, - T_PROTECTED, - T_PRIVATE, - T_STATIC, - T_FINAL, - T_ABSTRACT, - T_WHITESPACE, - ); - - $doc_block_end_token = $phpcsFile->findPrevious( $tokens_before_the_docblock, ( $stackPtr - 1 ), null, true, null, true ); - if ( ( false === $doc_block_end_token ) || ( T_DOC_COMMENT_CLOSE_TAG !== $tokens[ $doc_block_end_token ]['code'] ) ) { - $phpcsFile->addError( $missing_since_tag_error_message, $stackPtr, 'MissingSinceTag' ); - return; - } - - // The sniff intentionally doesn't check if the docblock has a valid open tag. - // Its only job is to make sure that the @since tag is present and has a valid version value. - $doc_block_start_token = $phpcsFile->findPrevious( Tokens::$commentTokens, ( $doc_block_end_token - 1 ), null, true, null, true ); - if ( false === $doc_block_start_token ) { - $phpcsFile->addError( $missing_since_tag_error_message, $stackPtr, 'MissingSinceTag' ); - return; - } - - // This is the first non-docblock token, so the next token should be used. - ++$doc_block_start_token; - - $since_tag_token = $phpcsFile->findNext( T_DOC_COMMENT_TAG, $doc_block_start_token, $doc_block_end_token, false, '@since', true ); - if ( false === $since_tag_token ) { - $phpcsFile->addError( $missing_since_tag_error_message, $stackPtr, 'MissingSinceTag' ); - return; - } - - $version_token = $phpcsFile->findNext( T_DOC_COMMENT_WHITESPACE, $since_tag_token + 1, null, true, null, true ); - if ( ( false === $version_token ) || ( T_DOC_COMMENT_STRING !== $tokens[ $version_token ]['code'] ) ) { - $phpcsFile->addError( $missing_since_tag_error_message, $since_tag_token, 'MissingSinceTag' ); - return; - } - - $version_value = $tokens[ $version_token ]['content']; - - if ( version_compare( $version_value, '0.0.1', '>=' ) ) { - // Validate the version value. - return; - } - - $phpcsFile->addError( - 'Invalid @since version value for the "%s()" function: "%s". Version value must be greater than or equal to 0.0.1.', - $version_token, - 'InvalidSinceTagVersionValue', - array( - $function_name, - $version_value, - ) - ); - } - - /** - * Checks if the current block is experimental. - * - * @param File $phpcsFile The file being scanned. - * @return bool Returns true if the current block is experimental. - */ - private static function is_experimental_block( File $phpcsFile ) { - $block_json_filepath = dirname( $phpcsFile->getFilename() ) . DIRECTORY_SEPARATOR . 'block.json'; - - if ( isset( static::$cache[ $block_json_filepath ] ) ) { - return static::$cache[ $block_json_filepath ]; - } - - if ( ! is_file( $block_json_filepath ) || ! is_readable( $block_json_filepath ) ) { - static::$cache[ $block_json_filepath ] = false; - return static::$cache[ $block_json_filepath ]; - } - - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- this Composer package doesn't depend on WordPress. - $block_metadata = file_get_contents( $block_json_filepath ); - if ( false === $block_metadata ) { - static::$cache[ $block_json_filepath ] = false; - return static::$cache[ $block_json_filepath ]; - } - - $block_metadata = json_decode( $block_metadata, true ); - if ( ! is_array( $block_metadata ) ) { - static::$cache[ $block_json_filepath ] = false; - return static::$cache[ $block_json_filepath ]; - } - - $experimental_flag = '__experimental'; - static::$cache[ $block_json_filepath ] = array_key_exists( $experimental_flag, $block_metadata ) && ( false !== $block_metadata[ $experimental_flag ] ); - return static::$cache[ $block_json_filepath ]; - } -} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php new file mode 100644 index 0000000000000..f216f4f681f0e --- /dev/null +++ b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php @@ -0,0 +1,617 @@ + array( + 'name' => 'class', + ), + T_INTERFACE => array( + 'name' => 'interface', + ), + T_TRAIT => array( + 'name' => 'trait', + ), + T_ENUM => array( + 'name' => 'enum', + ), + ); + + /** + * This property is used to store results returned + * by the static::is_experimental_block() method. + * + * @var array + */ + protected static $cache = array(); + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + return array_merge( + array( + T_FUNCTION, + T_VARIABLE, + T_STRING, + ), + array_keys( static::$oo_tokens ) + ); + } + + /** + * Processes the tokens that this sniff is interested in. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + */ + public function process( File $phpcsFile, $stackPtr ) { + if ( static::is_experimental_block( $phpcsFile ) ) { + // The "@since" tag is not required for experimental blocks since they are not yet included in WordPress Core. + return; + } + + $tokens = $phpcsFile->getTokens(); + $token = $tokens[ $stackPtr ]; + + if ( 'T_FUNCTION' === $token['type'] ) { + $this->process_function_token( $phpcsFile, $stackPtr ); + return; + } + + if ( isset( static::$oo_tokens[ $token['code'] ] ) ) { + $this->process_oo_token( $phpcsFile, $stackPtr ); + return; + } + + if ( 'T_STRING' === $token['type'] && static::is_function_call( $phpcsFile, $stackPtr ) ) { + $this->process_hook( $phpcsFile, $stackPtr ); + return; + } + + if ( 'T_VARIABLE' === $token['type'] && Scopes::isOOProperty( $phpcsFile, $stackPtr ) ) { + $this->process_property_token( $phpcsFile, $stackPtr ); + } + } + + /** + * Processes a token representing a function call that invokes a WordPress hook, + * checking for a missing `@since` tag in its docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the hook token in the stack. + */ + protected function process_hook( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + + // The content of the current token. + $hook_function = $tokens[ $stack_pointer ]['content']; + + $hook_invocation_functions = array( + 'do_action', + 'do_action_ref_array', + 'do_action_deprecated', + 'apply_filters', + 'apply_filters_ref_array', + 'apply_filters_deprecated', + ); + + // Check if the current token content is one of the filter functions. + if ( ! in_array( $hook_function, $hook_invocation_functions, true ) ) { + // Not a hook. + return; + } + + $error_message_data = array( $hook_function ); + + $violation_codes = static::get_violation_codes( 'Hook' ); + + $docblock = static::find_hook_docblock( $phpcs_file, $stack_pointer ); + + $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); + if ( empty( $version_tags ) ) { + if ( false !== $docblock ) { + $docblock_content = GetTokensAsString::compact( $phpcs_file, $docblock['start_token'], $docblock['end_token'], false ); + if ( false !== stripos( $docblock_content, 'This filter is documented in ' ) ) { + $hook_documented_elsewhere = true; + } + } + + if ( empty( $hook_documented_elsewhere ) ) { + $phpcs_file->addError( + 'Missing @since tag for the "%s()" hook function.', + $stack_pointer, + $violation_codes['missing_since_tag'], + $error_message_data + ); + } + + return; + } + + foreach ( $version_tags as $since_tag_token => $version_value_token ) { + if ( null === $version_value_token ) { + $phpcs_file->addError( + 'Missing @since tag version value for the "%s()" hook function.', + $since_tag_token, + $violation_codes['missing_version_value'], + $error_message_data + ); + continue; + } + + $version_value = $tokens[ $version_value_token ]['content']; + + if ( static::validate_version( $version_value ) ) { + continue; + } + + $phpcs_file->addError( + 'Invalid @since version value for the "%s()" hook function: "%s". Version value must be greater than or equal to 0.0.1.', + $version_value_token, + $violation_codes['invalid_version_value'], + array_merge( $error_message_data, array( $version_value ) ) + ); + } + } + + /** + * Processes a token representing an object-oriented programming structure + * like a class, interface, trait, or enum to check for a missing `@since` tag in its docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the OO token in the stack. + */ + protected function process_oo_token( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + $token_type = static::$oo_tokens[ $tokens[ $stack_pointer ]['code'] ]['name']; + + $token_name = ObjectDeclarations::getName( $phpcs_file, $stack_pointer ); + $error_message_data = array( + $token_name, + $token_type, + ); + + $violation_codes = static::get_violation_codes( ucfirst( $token_type ) ); + + $docblock = static::find_docblock( $phpcs_file, $stack_pointer ); + + $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); + if ( empty( $version_tags ) ) { + $phpcs_file->addError( + 'Missing @since tag for the "%s" %s.', + $stack_pointer, + $violation_codes['missing_since_tag'], + $error_message_data + ); + return; + } + + foreach ( $version_tags as $since_tag_token => $version_value_token ) { + if ( null === $version_value_token ) { + $phpcs_file->addError( + 'Missing @since tag version value for the "%s" %s.', + $since_tag_token, + $violation_codes['missing_version_value'], + $error_message_data + ); + continue; + } + + $version_value = $tokens[ $version_value_token ]['content']; + + if ( static::validate_version( $version_value ) ) { + continue; + } + + $phpcs_file->addError( + 'Invalid @since version value for the "%s" %s: "%s". Version value must be greater than or equal to 0.0.1.', + $version_value_token, + $violation_codes['invalid_version_value'], + array_merge( $error_message_data, array( $version_value ) ) + ); + } + } + + /** + * Processes a token representing an object-oriented property to check for a missing @since tag in its docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the object-oriented property token in the stack. + */ + protected function process_property_token( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + + $property_name = $tokens[ $stack_pointer ]['content']; + $oo_token = Scopes::validDirectScope( $phpcs_file, $stack_pointer, Collections::ooPropertyScopes() ); + $class_name = ObjectDeclarations::getName( $phpcs_file, $oo_token ); + + $visibility = Variables::getMemberProperties( $phpcs_file, $stack_pointer )['scope']; + if ( $this->check_below_minimum_visibility( $visibility ) ) { + return; + } + + $violation_codes = static::get_violation_codes( 'Property' ); + + $error_message_data = array( + $class_name, + $property_name, + ); + + $docblock = static::find_docblock( $phpcs_file, $stack_pointer ); + + $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); + if ( empty( $version_tags ) ) { + $phpcs_file->addError( + 'Missing @since tag for the "%s::%s" property.', + $stack_pointer, + $violation_codes['missing_since_tag'], + $error_message_data + ); + return; + } + + foreach ( $version_tags as $since_tag_token => $version_value_token ) { + if ( null === $version_value_token ) { + $phpcs_file->addError( + 'Missing @since tag version value for the "%s::%s" property.', + $since_tag_token, + $violation_codes['missing_version_value'], + $error_message_data + ); + continue; + } + + $version_value = $tokens[ $version_value_token ]['content']; + + if ( static::validate_version( $version_value ) ) { + continue; + } + + $phpcs_file->addError( + 'Invalid @since version value for the "%s::%s" property: "%s". Version value must be greater than or equal to 0.0.1.', + $version_value_token, + $violation_codes['invalid_version_value'], + array_merge( $error_message_data, array( $version_value ) ) + ); + } + } + + /** + * Processes a T_FUNCTION token to check for a missing @since tag in its docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the T_FUNCTION token in the stack. + */ + protected function process_function_token( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + + $oo_token = Scopes::validDirectScope( $phpcs_file, $stack_pointer, Tokens::$ooScopeTokens ); + $function_name = ObjectDeclarations::getName( $phpcs_file, $stack_pointer ); + + $token_type = 'function'; + if ( Scopes::isOOMethod( $phpcs_file, $stack_pointer ) ) { + $visibility = FunctionDeclarations::getProperties( $phpcs_file, $stack_pointer )['scope']; + if ( $this->check_below_minimum_visibility( $visibility ) ) { + return; + } + + $function_name = ObjectDeclarations::getName( $phpcs_file, $oo_token ) . '::' . $function_name; + $token_type = 'method'; + } + + $violation_codes = static::get_violation_codes( ucfirst( $token_type ) ); + + $error_message_data = array( + $function_name, + $token_type, + ); + + $docblock = static::find_docblock( $phpcs_file, $stack_pointer ); + + $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); + if ( empty( $version_tags ) ) { + $phpcs_file->addError( + 'Missing @since tag for the "%s()" %s.', + $stack_pointer, + $violation_codes['missing_since_tag'], + $error_message_data + ); + return; + } + + foreach ( $version_tags as $since_tag_token => $version_value_token ) { + if ( null === $version_value_token ) { + $phpcs_file->addError( + 'Missing @since tag version value for the "%s()" %s.', + $since_tag_token, + $violation_codes['missing_version_value'], + $error_message_data + ); + continue; + } + + $version_value = $tokens[ $version_value_token ]['content']; + + if ( static::validate_version( $version_value ) ) { + continue; + } + + $phpcs_file->addError( + 'Invalid @since version value for the "%s()" %s: "%s". Version value must be greater than or equal to 0.0.1.', + $version_value_token, + $violation_codes['invalid_version_value'], + array_merge( $error_message_data, array( $version_value ) ) + ); + } + } + + /** + * Validates the version value. + * + * @param string $version The version value being checked. + * @return bool True if the version value is valid. + */ + protected static function validate_version( $version ) { + $matches = array(); + if ( 1 === preg_match( '/^MU \((?.+)\)/', $version, $matches ) ) { + $version = $matches['version']; + } + + return version_compare( $version, '0.0.1', '>=' ); + } + + + /** + * Returns violation codes for a specific token type. + * + * @param string $token_type The type of token (e.g., Function, Property) to retrieve violation codes for. + * @return array An array containing violation codes for missing since tag, missing version value, and invalid version value. + */ + protected static function get_violation_codes( $token_type ) { + return array( + 'missing_since_tag' => 'Missing' . $token_type . 'SinceTag', + 'missing_version_value' => 'Missing' . $token_type . 'VersionValue', + 'invalid_version_value' => 'Invalid' . $token_type . 'VersionValue', + ); + } + + /** + * Checks if the provided visibility level is below the set minimum visibility level. + * + * @param string $visibility The visibility level to check. + * @return bool Returns true if the provided visibility level is below the minimum visibility level, false otherwise. + */ + protected function check_below_minimum_visibility( $visibility ) { + if ( 'public' === $this->minimumVisibility && in_array( $visibility, array( 'protected', 'private' ), true ) ) { + return true; + } + + if ( 'protected' === $this->minimumVisibility && 'private' === $visibility ) { + return true; + } + + return false; + } + + /** + * Finds the docblock associated with a hook, starting from a specified position in the token stack. + * Since a line containing a hook can include any type of tokens, this method backtracks through the tokens + * to locate the first token on the current line. This token is then used as the starting point for searching the docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position to start looking for the docblock. + * @return array|false An associative array containing the start and end tokens of the docblock, or false if not found. + */ + protected static function find_hook_docblock( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + $current_line = $tokens[ $stack_pointer ]['line']; + + for ( $i = $stack_pointer; $i >= 0; $i-- ) { + if ( $tokens[ $i ]['line'] < $current_line ) { + // The previous token is on the previous line, so the current token is the first on the line. + return static::find_docblock( $phpcs_file, $i + 1 ); + } + } + + return static::find_docblock( $phpcs_file, 0 ); + } + + /** + * Determines if a T_STRING token represents a function call. + * The implementation was copied from PHPCompatibility\Sniffs\Extensions\RemovedExtensionsSniff::process(). + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the T_STRING token in question. + * @return bool True if the token represents a function call, false otherwise. + */ + protected static function is_function_call( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + + // Find the next non-empty token. + $open_bracket = $phpcs_file->findNext( Tokens::$emptyTokens, ( $stack_pointer + 1 ), null, true ); + + if ( T_OPEN_PARENTHESIS !== $tokens[ $open_bracket ]['code'] ) { + // Not a function call. + return false; + } + + if ( false === isset( $tokens[ $open_bracket ]['parenthesis_closer'] ) ) { + // Not a function call. + return false; + } + + // Find the previous non-empty token. + $search = Tokens::$emptyTokens; + $search[] = T_BITWISE_AND; + $previous = $phpcs_file->findPrevious( $search, ( $stack_pointer - 1 ), null, true ); + + $previous_tokens_to_ignore = array( + T_FUNCTION, // Function declaration. + T_NEW, // Creating an object. + T_OBJECT_OPERATOR, // Calling an object. + ); + + return ! in_array( $tokens[ $previous ]['code'], $previous_tokens_to_ignore, true ); + } + + /** + * Finds the docblock preceding a specified position (stack pointer) in a given PHP file. + * The implementation was copied from PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff::process(). + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position (stack pointer) in the token stack from which to start searching backwards. + * @return array|false An associative array containing the start and end tokens of the docblock, or false if not found. + */ + protected static function find_docblock( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + $ignore = Tokens::$methodPrefixes; + $ignore[ T_WHITESPACE ] = T_WHITESPACE; + + for ( $comment_end = ( $stack_pointer - 1 ); $comment_end >= 0; $comment_end-- ) { + if ( isset( $ignore[ $tokens[ $comment_end ]['code'] ] ) ) { + continue; + } + + if ( T_ATTRIBUTE_END === $tokens[ $comment_end ]['code'] + && isset( $tokens[ $comment_end ]['attribute_opener'] ) + ) { + $comment_end = $tokens[ $comment_end ]['attribute_opener']; + continue; + } + + break; + } + + if ( $tokens[ $comment_end ]['code'] === T_COMMENT ) { + // Inline comments might just be closing comments for + // control structures or functions instead of function comments + // using the wrong comment type. If there is other code on the line, + // assume they relate to that code. + $previous = $phpcs_file->findPrevious( $ignore, ( $comment_end - 1 ), null, true ); + if ( false !== $previous && $tokens[ $previous ]['line'] === $tokens[ $comment_end ]['line'] ) { + $comment_end = $previous; + } + } + + if ( T_DOC_COMMENT_CLOSE_TAG !== $tokens[ $comment_end ]['code'] ) { + // Only "/**" style comments are supported. + return false; + } + + return array( + 'start_token' => $tokens[ $comment_end ]['comment_opener'], + 'end_token' => $comment_end, + ); + } + + /** + * Searches for @since values within a docblock. + * + * @param File $phpcs_file The file being scanned. + * @param array|false $docblock An associative array containing the start and end tokens of the docblock, or false if not exists. + * @return array Returns an array of "@since" tokens and their corresponding value tokens. + */ + protected static function parse_since_tags( File $phpcs_file, $docblock ) { + $version_tags = array(); + + if ( false === $docblock ) { + return $version_tags; + } + + $tokens = $phpcs_file->getTokens(); + + for ( $i = $docblock['start_token'] + 1; $i < $docblock['end_token']; $i++ ) { + if ( ! ( T_DOC_COMMENT_TAG === $tokens[ $i ]['code'] && '@since' === $tokens[ $i ]['content'] ) ) { + continue; + } + + $version_token = $phpcs_file->findNext( T_DOC_COMMENT_WHITESPACE, $i + 1, $docblock['end_token'], true, null, true ); + if ( ( false === $version_token ) || ( T_DOC_COMMENT_STRING !== $tokens[ $version_token ]['code'] ) ) { + $version_tags[ $i ] = null; + continue; + } + + $version_tags[ $i ] = $version_token; + } + + return $version_tags; + } + + /** + * Checks if the current block is experimental. + * + * @param File $phpcs_file The file being scanned. + * @return bool Returns true if the current block is experimental. + */ + protected static function is_experimental_block( File $phpcs_file ) { + $block_json_filepath = dirname( $phpcs_file->getFilename() ) . DIRECTORY_SEPARATOR . 'block.json'; + + if ( isset( static::$cache[ $block_json_filepath ] ) ) { + return static::$cache[ $block_json_filepath ]; + } + + if ( ! is_file( $block_json_filepath ) || ! is_readable( $block_json_filepath ) ) { + static::$cache[ $block_json_filepath ] = false; + return static::$cache[ $block_json_filepath ]; + } + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- this Composer package doesn't depend on WordPress. + $block_metadata = file_get_contents( $block_json_filepath ); + if ( false === $block_metadata ) { + static::$cache[ $block_json_filepath ] = false; + return static::$cache[ $block_json_filepath ]; + } + + $block_metadata = json_decode( $block_metadata, true ); + if ( ! is_array( $block_metadata ) ) { + static::$cache[ $block_json_filepath ] = false; + return static::$cache[ $block_json_filepath ]; + } + + $experimental_flag = '__experimental'; + static::$cache[ $block_json_filepath ] = array_key_exists( $experimental_flag, $block_metadata ) && ( false !== $block_metadata[ $experimental_flag ] ); + return static::$cache[ $block_json_filepath ]; + } +} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php new file mode 100644 index 0000000000000..08838ce412fc3 --- /dev/null +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php @@ -0,0 +1,85 @@ +get_sniff_fqcn(); + if ( ! isset( $current_ruleset->sniffs[ $sniff_fqcn ] ) ) { + throw new \RuntimeException( $error_message ); + } + + $sniff = $current_ruleset->sniffs[ $sniff_fqcn ]; + $this->set_sniff_parameters( $sniff ); + } +} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php index 5073cea9ecf06..8026e88f1d945 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php @@ -10,22 +10,14 @@ namespace GutenbergCS\Gutenberg\Tests\CodeAnalysis; use GutenbergCS\Gutenberg\Sniffs\CodeAnalysis\ForbiddenFunctionsAndClassesSniff; -use PHP_CodeSniffer\Config; -use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use PHP_CodeSniffer\Ruleset; +use GutenbergCS\Gutenberg\Tests\AbstractSniffUnitTest; +use PHP_CodeSniffer\Sniffs\Sniff; /** * Unit test class for the ForbiddenFunctionsAndClassesSniff sniff. */ final class ForbiddenFunctionsAndClassesUnitTest extends AbstractSniffUnitTest { - /** - * Holds the original Ruleset instance. - * - * @var Ruleset - */ - private static $original_ruleset; - /** * Returns the lines where errors should occur. * @@ -73,50 +65,22 @@ public function getWarningList() { } /** + * Returns the fully qualified class name (FQCN) of the sniff. * - * This method resets the 'Gutenberg' ruleset in the $GLOBALS['PHP_CODESNIFFER_RULESETS'] - * to its original state. + * @return string The fully qualified class name of the sniff. */ - public static function tearDownAfterClass() { - parent::tearDownAfterClass(); - - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = self::$original_ruleset; - self::$original_ruleset = null; + protected function get_sniff_fqcn() { + return ForbiddenFunctionsAndClassesSniff::class; } - /** - * Prepares the environment before executing tests. Specifically, sets prefixes for the - * ForbiddenFunctionsAndClassesSniff sniff.This is needed since AbstractSniffUnitTest class - * doesn't apply sniff properties from the Gutenberg/ruleset.xml file. + * Sets the parameters for the sniff. * - * @param string $filename The name of the file being tested. - * @param Config $config The config data for the run. + * @throws RuntimeException If unable to set the ruleset parameters required for the test. * - * @return void + * @param Sniff $sniff The sniff being tested. */ - public function setCliValues( $filename, $config ) { - parent::setCliValues( $filename, $config ); - - if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] ) - || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - // Backup the original Ruleset instance. - self::$original_ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg']; - - $current_ruleset = clone self::$original_ruleset; - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = $current_ruleset; - - if ( ! isset( $current_ruleset->sniffs[ ForbiddenFunctionsAndClassesSniff::class ] ) - || ( ! $current_ruleset->sniffs[ ForbiddenFunctionsAndClassesSniff::class ] instanceof ForbiddenFunctionsAndClassesSniff ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - $sniff = $current_ruleset->sniffs[ ForbiddenFunctionsAndClassesSniff::class ]; + public function set_sniff_parameters( Sniff $sniff ) { $sniff->forbidden_functions = array( '[Gg]utenberg.*', ); diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php index fdd5c07c8cb59..652f6b735378c 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php @@ -10,22 +10,14 @@ namespace GutenbergCS\Gutenberg\Tests\CodeAnalysis; use GutenbergCS\Gutenberg\Sniffs\CodeAnalysis\GuardedFunctionAndClassNamesSniff; -use PHP_CodeSniffer\Config; -use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use PHP_CodeSniffer\Ruleset; +use GutenbergCS\Gutenberg\Tests\AbstractSniffUnitTest; +use PHP_CodeSniffer\Sniffs\Sniff; /** * Unit test class for the GuardedFunctionAndClassNames sniff. */ final class GuardedFunctionAndClassNamesUnitTest extends AbstractSniffUnitTest { - /** - * Holds the original Ruleset instance. - * - * @var Ruleset - */ - private static $original_ruleset; - /** * Returns the lines where errors should occur. * @@ -50,50 +42,22 @@ public function getWarningList() { } /** + * Returns the fully qualified class name (FQCN) of the sniff. * - * This method resets the 'Gutenberg' ruleset in the $GLOBALS['PHP_CODESNIFFER_RULESETS'] - * to its original state. + * @return string The fully qualified class name of the sniff. */ - public static function tearDownAfterClass() { - parent::tearDownAfterClass(); - - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = self::$original_ruleset; - self::$original_ruleset = null; + protected function get_sniff_fqcn() { + return GuardedFunctionAndClassNamesSniff::class; } - /** - * Prepares the environment before executing tests. Specifically, sets prefixes for the - * GuardedFunctionAndClassNames sniff.This is needed since AbstractSniffUnitTest class - * doesn't apply sniff properties from the Gutenberg/ruleset.xml file. + * Sets the parameters for the sniff. * - * @param string $filename The name of the file being tested. - * @param Config $config The config data for the run. + * @throws RuntimeException If unable to set the ruleset parameters required for the test. * - * @return void + * @param Sniff $sniff The sniff being tested. */ - public function setCliValues( $filename, $config ) { - parent::setCliValues( $filename, $config ); - - if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] ) - || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - // Backup the original Ruleset instance. - self::$original_ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg']; - - $current_ruleset = clone self::$original_ruleset; - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = $current_ruleset; - - if ( ! isset( $current_ruleset->sniffs[ GuardedFunctionAndClassNamesSniff::class ] ) - || ( ! $current_ruleset->sniffs[ GuardedFunctionAndClassNamesSniff::class ] instanceof GuardedFunctionAndClassNamesSniff ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - $sniff = $current_ruleset->sniffs[ GuardedFunctionAndClassNamesSniff::class ]; + public function set_sniff_parameters( Sniff $sniff ) { $sniff->functionsWhiteList = array( '/^_?gutenberg.+/', ); diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.inc b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.inc deleted file mode 100644 index 43d11694bb25e..0000000000000 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.inc +++ /dev/null @@ -1,39 +0,0 @@ - => - */ - public function getErrorList() { - // The sniff only supports PHP functions for now; it ignores class, trait, and interface methods. - return array( - 9 => 1, - 19 => 1, - 24 => 1, - ); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array => - */ - public function getWarningList() { - return array(); - } -} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.inc b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.inc new file mode 100644 index 0000000000000..aa8593b2e2757 --- /dev/null +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.inc @@ -0,0 +1,629 @@ +do_action(); +$foo = new do_action_ref_array(); +$foo->do_action_ref_array(); +$foo = new do_action_deprecated(); +$foo->do_action_deprecated(); +$foo = new apply_filters(); +$foo->apply_filters(); +$foo = new apply_filters_ref_array(); +$foo->apply_filters_ref_array(); +$foo = new apply_filters_deprecated(); +$foo->apply_filters_deprecated(); +$foo = new non_hook_action(); +$foo->non_hook_action(); diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php new file mode 100644 index 0000000000000..bc7ca28c263ff --- /dev/null +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php @@ -0,0 +1,189 @@ + => + */ + public function getErrorList() { + return array( + 2 => 1, + 3 => 1, + 4 => 1, + 5 => 1, + 6 => 1, + 9 => 1, + 15 => 1, + 26 => 1, + 33 => 1, + 35 => 1, + 36 => 1, + 42 => 1, + 49 => 1, + 62 => 1, + 67 => 1, + 69 => 1, + 70 => 1, + 79 => 1, + 82 => 1, + 88 => 1, + 97 => 1, + 99 => 1, + 105 => 1, + 107 => 1, + 108 => 1, + 112 => 1, + 113 => 1, + 114 => 1, + 115 => 1, + 116 => 1, + 119 => 1, + 125 => 1, + 136 => 1, + 142 => 1, + 145 => 1, + 152 => 1, + 165 => 1, + 174 => 1, + 178 => 1, + 180 => 1, + 181 => 1, + 188 => 1, + 195 => 1, + 208 => 1, + 213 => 1, + 215 => 1, + 216 => 1, + 221 => 1, + 223 => 1, + 229 => 1, + 238 => 1, + 240 => 1, + 246 => 1, + 248 => 1, + 249 => 1, + 253 => 1, + 254 => 1, + 255 => 1, + 256 => 1, + 257 => 1, + 260 => 1, + 266 => 1, + 277 => 1, + 283 => 1, + 286 => 1, + 293 => 1, + 306 => 1, + 315 => 1, + 319 => 1, + 321 => 1, + 322 => 1, + 329 => 1, + 336 => 1, + 349 => 1, + 354 => 1, + 356 => 1, + 357 => 1, + 362 => 1, + 365 => 1, + 371 => 1, + 380 => 1, + 382 => 1, + 388 => 1, + 390 => 1, + 391 => 1, + 395 => 1, + 396 => 1, + 397 => 1, + 398 => 1, + 399 => 1, + 402 => 1, + 408 => 1, + 419 => 1, + 426 => 1, + 433 => 1, + 446 => 1, + 455 => 1, + 459 => 1, + 461 => 1, + 462 => 1, + 469 => 1, + 476 => 1, + 489 => 1, + 492 => 1, + 493 => 1, + 496 => 1, + 502 => 1, + 513 => 1, + 517 => 1, + 519 => 1, + 520 => 1, + 526 => 1, + 533 => 1, + 546 => 1, + 549 => 1, + 550 => 1, + 551 => 1, + 552 => 1, + 555 => 1, + 561 => 1, + 572 => 1, + 579 => 1, + 581 => 1, + 582 => 1, + 587 => 1, + 592 => 1, + 597 => 1, + 602 => 1, + 607 => 1, + 612 => 1, + ); + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() { + return array(); + } + + /** + * Returns the fully qualified class name (FQCN) of the sniff. + * + * @return string The fully qualified class name of the sniff. + */ + protected function get_sniff_fqcn() { + return SinceTagSniff::class; + } + + /** + * Sets the parameters for the sniff. + * + * @throws RuntimeException If unable to set the ruleset parameters required for the test. + * + * @param Sniff $sniff The sniff being tested. + */ + public function set_sniff_parameters( Sniff $sniff ) { + $sniff->minimumVisibility = 'protected'; + } +} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php index 794a088b7bc61..51174dd769d0a 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php @@ -10,22 +10,14 @@ namespace GutenbergCS\Gutenberg\Tests\NamingConventions; use GutenbergCS\Gutenberg\Sniffs\NamingConventions\ValidBlockLibraryFunctionNameSniff; -use PHP_CodeSniffer\Config; -use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use PHP_CodeSniffer\Ruleset; +use GutenbergCS\Gutenberg\Tests\AbstractSniffUnitTest; +use PHP_CodeSniffer\Sniffs\Sniff; /** * Unit test class for the ValidBlockLibraryFunctionNameSniff sniff. */ final class ValidBlockLibraryFunctionNameUnitTest extends AbstractSniffUnitTest { - /** - * Holds the original Ruleset instance. - * - * @var Ruleset - */ - private static $original_ruleset; - /** * Returns the lines where errors should occur. * @@ -50,50 +42,22 @@ public function getWarningList() { } /** + * Returns the fully qualified class name (FQCN) of the sniff. * - * This method resets the 'Gutenberg' ruleset in the $GLOBALS['PHP_CODESNIFFER_RULESETS'] - * to its original state. + * @return string The fully qualified class name of the sniff. */ - public static function tearDownAfterClass() { - parent::tearDownAfterClass(); - - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = self::$original_ruleset; - self::$original_ruleset = null; + protected function get_sniff_fqcn() { + return ValidBlockLibraryFunctionNameSniff::class; } - /** - * Prepares the environment before executing tests. Specifically, sets prefixes for the - * ValidBlockLibraryFunctionName sniff.This is needed since AbstractSniffUnitTest class - * doesn't apply sniff properties from the Gutenberg/ruleset.xml file. + * Sets the parameters for the sniff. * - * @param string $filename The name of the file being tested. - * @param Config $config The config data for the run. + * @throws RuntimeException If unable to set the ruleset parameters required for the test. * - * @return void + * @param Sniff $sniff The sniff being tested. */ - public function setCliValues( $filename, $config ) { - parent::setCliValues( $filename, $config ); - - if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] ) - || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - // Backup the original Ruleset instance. - self::$original_ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg']; - - $current_ruleset = clone self::$original_ruleset; - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = $current_ruleset; - - if ( ! isset( $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ] ) - || ( ! $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ] instanceof ValidBlockLibraryFunctionNameSniff ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - $sniff = $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ]; + public function set_sniff_parameters( Sniff $sniff ) { $sniff->prefixes = array( 'block_core_', 'render_block_core_', diff --git a/test/php/gutenberg-coding-standards/Gutenberg/ruleset.xml b/test/php/gutenberg-coding-standards/Gutenberg/ruleset.xml index 74400e0e6d5cd..503f7e09b85d7 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/ruleset.xml +++ b/test/php/gutenberg-coding-standards/Gutenberg/ruleset.xml @@ -6,5 +6,6 @@ + diff --git a/test/php/gutenberg-coding-standards/composer.json b/test/php/gutenberg-coding-standards/composer.json index 0aeec177918c0..c1c27f81818aa 100644 --- a/test/php/gutenberg-coding-standards/composer.json +++ b/test/php/gutenberg-coding-standards/composer.json @@ -20,7 +20,8 @@ "ext-libxml": "*", "ext-tokenizer": "*", "ext-xmlreader": "*", - "squizlabs/php_codesniffer": "^3.7.2" + "squizlabs/php_codesniffer": "^3.7.2", + "phpcsstandards/phpcsutils": "^1.0.8" }, "require-dev": { "phpcompatibility/php-compatibility": "^9.0",