diff --git a/.github/release-checklist.md b/.github/release-checklist.md index 108bea3aa..92f93e024 100644 --- a/.github/release-checklist.md +++ b/.github/release-checklist.md @@ -28,7 +28,8 @@ PR for tracking changes for the x.x.x release. Target release date: **DOW MONTH - [ ] `$pluggable_functions` in `WordPress.NamingConventions.PrefixAllGlobals` - PR #xxx - [ ] `$pluggable_classes` in `WordPress.NamingConventions.PrefixAllGlobals` - PR #xxx - [ ] `$target_functions` in `WordPress.Security.PluginMenuSlug` - PR #xxx - - [ ] `$reserved_names` in `WordPress.NamingConventions.ValidPostTypeSlug` - PR #xxx + - [ ] `$post_types` in `WPReservedNamesHelper` - PR #xxx + - [ ] `$terms` in `WPReservedNamesHelper` - PR #xxx - [ ] `$wp_time_constants` in `WordPress.WP.CronInterval` - PR #xxx - [ ] `$known_test_classes` in `IsUnitTestTrait` - PR #xxx - [ ] ...etc... diff --git a/WordPress-Extra/ruleset.xml b/WordPress-Extra/ruleset.xml index 53da8be01..2f10ba81d 100644 --- a/WordPress-Extra/ruleset.xml +++ b/WordPress-Extra/ruleset.xml @@ -129,9 +129,12 @@ - + + + + diff --git a/WordPress/AbstractValidSlugSniff.php b/WordPress/AbstractValidSlugSniff.php new file mode 100644 index 000000000..5061b1ee3 --- /dev/null +++ b/WordPress/AbstractValidSlugSniff.php @@ -0,0 +1,300 @@ + Key is reserved name, value irrelevant. + */ + private $reserved_names; + + /** + * All valid tokens for the slug parameter. + * + * Set in `register()`. + * + * @since 3.2.0 + * + * @var array + */ + private $valid_tokens = array(); + + /** + * Retrieve function and parameter(s) pairs this sniff is looking for. + * + * The parameter or an array of parameters keyed by target function. + * An array of names is supported to allow for functions for which the + * parameter names have undergone name changes over time. + * + * @since 3.2.0 + * + * @return array> Function parameter(s) pairs. + */ + abstract protected function get_target_functions(); + + /** + * Retrieve the slug type. + * + * @since 3.2.0 + * + * @return string The slug type. + */ + abstract protected function get_slug_type(); + + /** + * Retrieve the plural slug type. + * + * @since 3.2.0 + * + * @return string The plural slug type. + */ + abstract protected function get_slug_type_plural(); + + /** + * Retrieve the regex to validate the characters that can be used as + * the slug. + * + * @since 3.2.0 + * + * @return string Regular expression. + */ + abstract protected function get_valid_characters(); + + /** + * Retrieve the max length of a slug. + * + * @since 3.2.0 + * + * @return int The maximum length of a slug. + */ + abstract protected function get_max_length(); + + /** + * Retrieve the reserved names which can not be used by themes and plugins. + * + * @since 3.2.0 + * + * @return array Key is reserved name, value irrelevant. + */ + abstract protected function get_reserved_names(); + + /** + * Returns an array of tokens this test wants to listen for. + * + * @since 2.2.0 + * @since 3.2.0 - Moved from the ValidPostTypeSlug Sniff class to this class. + * - Added setting up properties for target functions and slug details. + * + * @return array + */ + public function register() { + $this->valid_tokens = Tokens::$textStringTokens + Tokens::$heredocTokens + Tokens::$emptyTokens; + $this->target_functions = $this->get_target_functions(); + $this->slug_type = $this->get_slug_type(); + $this->slug_type_plural = $this->get_slug_type_plural(); + $this->valid_characters = $this->get_valid_characters(); + $this->max_length = $this->get_max_length(); + $this->reserved_names = $this->get_reserved_names(); + return parent::register(); + } + + /** + * Process the parameter of a matched function. + * + * Errors on invalid names when reserved names are used, + * the name is too long, or contains invalid characters. + * + * @since 2.2.0 + * @since 3.2.0 Moved from the ValidPostTypeSlug Sniff class to this class and + * modfied for variable target functions and slug restrictions. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $slug_param = PassedParameters::getParameterFromStack( + $parameters, + 1, + $this->target_functions[ $matched_content ] + ); + if ( false === $slug_param || '' === $slug_param['clean'] ) { + // Error for using empty slug. + $this->phpcsFile->addError( + '%s() called without a %s slug. The slug must be a non-empty string.', + false === $slug_param ? $stackPtr : $slug_param['start'], + 'Empty', + array( + $matched_content, + $this->slug_type, + ) + ); + return; + } + + $string_start = $this->phpcsFile->findNext( Collections::textStringStartTokens(), $slug_param['start'], ( $slug_param['end'] + 1 ) ); + $string_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $slug_param['start'], ( $slug_param['end'] + 1 ) ); + + $has_invalid_tokens = $this->phpcsFile->findNext( $this->valid_tokens, $slug_param['start'], ( $slug_param['end'] + 1 ), true ); + if ( false !== $has_invalid_tokens || false === $string_pos ) { + // Check for non string based slug parameter (we cannot determine if this is valid). + $this->phpcsFile->addWarning( + 'The %s slug is not a string literal. It is not possible to automatically determine the validity of this slug. Found: %s.', + $stackPtr, + 'NotStringLiteral', + array( + $this->slug_type, + $slug_param['clean'], + ), + 3 + ); + return; + } + + $slug = TextStrings::getCompleteTextString( $this->phpcsFile, $string_start ); + if ( isset( Tokens::$heredocTokens[ $this->tokens[ $string_start ]['code'] ] ) ) { + // Trim off potential indentation from PHP 7.3 flexible heredoc/nowdoc content. + $slug = ltrim( $slug ); + } + + // Warn for dynamic parts in the slug parameter. + if ( 'T_DOUBLE_QUOTED_STRING' === $this->tokens[ $string_pos ]['type'] + || ( 'T_HEREDOC' === $this->tokens[ $string_pos ]['type'] + && strpos( $this->tokens[ $string_pos ]['content'], '$' ) !== false ) + ) { + $this->phpcsFile->addWarning( + 'The %s slug may, or may not, get too long with dynamic contents and could contain invalid characters. Found: "%s".', + $string_pos, + 'PartiallyDynamic', + array( + $this->slug_type, + $slug, + ) + ); + $slug = TextStrings::stripEmbeds( $slug ); + } + + if ( preg_match( $this->valid_characters, $slug ) === 0 ) { + // Error for invalid characters. + $this->phpcsFile->addError( + '%s() called with invalid %s "%s". %s contains invalid characters. Only lowercase alphanumeric characters, dashes, and underscores are allowed.', + $string_pos, + 'InvalidCharacters', + array( + $matched_content, + $this->slug_type, + $slug, + ucfirst( $this->slug_type ), + ) + ); + } + + if ( isset( $this->reserved_names[ $slug ] ) ) { + // Error for using reserved slug names. + $this->phpcsFile->addError( + '%s() called with reserved %s "%s". Reserved %s should not be used as they interfere with the functioning of WordPress itself.', + $string_pos, + 'Reserved', + array( + $matched_content, + $this->slug_type, + $slug, + $this->slug_type_plural, + ) + ); + } elseif ( stripos( $slug, 'wp_' ) === 0 ) { + // Error for using reserved slug prefix. + $this->phpcsFile->addError( + 'The %s passed to %s() uses a prefix reserved for WordPress itself. Found: "%s".', + $string_pos, + 'ReservedPrefix', + array( + $this->slug_type, + $matched_content, + $slug, + ) + ); + } + + // Error for slugs that are too long. + if ( strlen( $slug ) > $this->max_length ) { + $this->phpcsFile->addError( + 'A %s slug must not exceed %d characters. Found: "%s" (%d characters).', + $string_pos, + 'TooLong', + array( + $this->slug_type, + $this->max_length, + $slug, + strlen( $slug ), + ) + ); + } + } +} diff --git a/WordPress/Docs/NamingConventions/ValidPostTypeSlugStandard.xml b/WordPress/Docs/NamingConventions/ValidPostTypeSlugStandard.xml index 639dda54e..f770b6870 100644 --- a/WordPress/Docs/NamingConventions/ValidPostTypeSlugStandard.xml +++ b/WordPress/Docs/NamingConventions/ValidPostTypeSlugStandard.xml @@ -11,17 +11,17 @@ 'my_short_slug', - array() +register_post_type( + 'my_short_slug', + array() ); ]]> 'my_own_post_type_too_long', - array() +register_post_type( + 'my_own_post_type_too_long', + array() ); ]]> @@ -34,17 +34,17 @@ register_post_type( 'my_post_type_slug', - array() +register_post_type( + 'my_post_type_slug', + array() ); ]]> 'my/post/type/slug', - array() +register_post_type( + 'my/post/type/slug', + array() ); ]]> @@ -57,40 +57,40 @@ register_post_type( 'my_post_active', - array() +register_post_type( + 'my_post_active', + array() ); ]]> "my_post_{$status}", - array() +register_post_type( + "my_post_{$status}", + array() ); ]]> 'prefixed_author', - array() +register_post_type( + 'prefixed_author', + array() ); ]]> - + 'author', - array() +register_post_type( + 'author', + array() ); ]]> @@ -103,17 +103,17 @@ register_post_type( 'prefixed_author', - array() +register_post_type( + 'prefixed_author', + array() ); ]]> 'wp_author', - array() +register_post_type( + 'wp_author', + array() ); ]]> diff --git a/WordPress/Docs/NamingConventions/ValidTaxonomySlugStandard.xml b/WordPress/Docs/NamingConventions/ValidTaxonomySlugStandard.xml new file mode 100644 index 000000000..25f525b2f --- /dev/null +++ b/WordPress/Docs/NamingConventions/ValidTaxonomySlugStandard.xml @@ -0,0 +1,121 @@ + + + + + + + + 'my_short_slug', + array() +); + ]]> + + + 'my_very_own_taxonomy_is_much_too_long', + array() +); + ]]> + + + + + + + + 'my_taxonomy_slug', + array() +); + ]]> + + + 'my/taxonomy/slug', + array() +); + ]]> + + + + + + + + 'my_taxonomy_active', + array() +); + ]]> + + + "my_taxonomy_{$status}", + array() +); + ]]> + + + + + + + + 'prefixed_post_tag', + array() +); + ]]> + + + 'post_tag', + array() +); + ]]> + + + + + + + + 'prefixed_post_tag', + array() +); + ]]> + + + 'wp_post_tag', + array() +); + ]]> + + + diff --git a/WordPress/Helpers/WPReservedNamesHelper.php b/WordPress/Helpers/WPReservedNamesHelper.php new file mode 100644 index 000000000..5ced2f3ae --- /dev/null +++ b/WordPress/Helpers/WPReservedNamesHelper.php @@ -0,0 +1,203 @@ + Key is reserved post type name, value irrelevant. + */ + private static $post_types = array( + 'action' => true, // Not a WP post type, but prevents other problems. + 'attachment' => true, + 'author' => true, // Not a WP post type, but prevents other problems. + 'custom_css' => true, + 'customize_changeset' => true, + 'nav_menu_item' => true, + 'oembed_cache' => true, + 'order' => true, // Not a WP post type, but prevents other problems. + 'page' => true, + 'post' => true, + 'revision' => true, + 'theme' => true, // Not a WP post type, but prevents other problems. + 'user_request' => true, + 'wp_block' => true, + 'wp_font_face' => true, + 'wp_font_family' => true, + 'wp_global_styles' => true, + 'wp_navigation' => true, + 'wp_template' => true, + 'wp_template_part' => true, + ); + + /** + * Array of reserved taxonomy names which can not be used by themes and plugins. + * + * Source: {@link https://developer.wordpress.org/reference/functions/register_taxonomy/#reserved-terms} + * + * {@internal To be updated after every major release. Last updated for WordPress 6.6.1.} + * + * @since 3.2.0 + * + * @var array Key is reserved taxonomy name, value irrelevant. + */ + private static $terms = array( + 'attachment' => true, + 'attachment_id' => true, + 'author' => true, + 'author_name' => true, + 'calendar' => true, + 'cat' => true, + 'category' => true, + 'category__and' => true, + 'category__in' => true, + 'category__not_in' => true, + 'category_name' => true, + 'comments_per_page' => true, + 'comments_popup' => true, + 'custom' => true, + 'customize_messenger_channel' => true, + 'customized' => true, + 'cpage' => true, + 'day' => true, + 'debug' => true, + 'embed' => true, + 'error' => true, + 'exact' => true, + 'feed' => true, + 'fields' => true, + 'hour' => true, + 'link_category' => true, + 'm' => true, + 'minute' => true, + 'monthnum' => true, + 'more' => true, + 'name' => true, + 'nav_menu' => true, + 'nonce' => true, + 'nopaging' => true, + 'offset' => true, + 'order' => true, + 'orderby' => true, + 'p' => true, + 'page' => true, + 'page_id' => true, + 'paged' => true, + 'pagename' => true, + 'pb' => true, + 'perm' => true, + 'post' => true, + 'post__in' => true, + 'post__not_in' => true, + 'post_format' => true, + 'post_mime_type' => true, + 'post_status' => true, + 'post_tag' => true, + 'post_type' => true, + 'posts' => true, + 'posts_per_archive_page' => true, + 'posts_per_page' => true, + 'preview' => true, + 'robots' => true, + 's' => true, + 'search' => true, + 'second' => true, + 'sentence' => true, + 'showposts' => true, + 'static' => true, + 'status' => true, + 'subpost' => true, + 'subpost_id' => true, + 'tag' => true, + 'tag__and' => true, + 'tag__in' => true, + 'tag__not_in' => true, + 'tag_id' => true, + 'tag_slug__and' => true, + 'tag_slug__in' => true, + 'taxonomy' => true, + 'tb' => true, + 'term' => true, + 'terms' => true, + 'theme' => true, + 'title' => true, + 'type' => true, + 'types' => true, + 'w' => true, + 'withcomments' => true, + 'withoutcomments' => true, + 'year' => true, + ); + + /** + * Verify if a given name is a reserved post type name. + * + * @since 3.2.0 + * + * @param string $name The name to be checked. + * + * @return bool + */ + public static function is_reserved_post_type( $name ) { + return isset( self::$post_types[ $name ] ); + } + + /** + * Verify if a given name is a reserved taxonomy name. + * + * @since 3.2.0 + * + * @param string $name The name to be checked. + * + * @return bool + */ + public static function is_reserved_term( $name ) { + return isset( self::$terms[ $name ] ) + || isset( self::$post_types[ $name ] ); + } + + /** + * Retrieve an array with the reserved post type names. + * + * @since 3.2.0 + * + * @return array Array with the post type names as keys. The value is irrelevant. + */ + public static function get_post_types() { + return self::$post_types; + } + + /** + * Retrieve an array with the reserved taxonomy names. + * + * @since 3.2.0 + * + * @return array Array with the taxonomy names as keys. The value is irrelevant. + */ + public static function get_terms() { + return array_merge( self::$post_types, self::$terms ); + } +} diff --git a/WordPress/Sniffs/NamingConventions/ValidPostTypeSlugSniff.php b/WordPress/Sniffs/NamingConventions/ValidPostTypeSlugSniff.php index 9b3d3db4b..c38da3158 100644 --- a/WordPress/Sniffs/NamingConventions/ValidPostTypeSlugSniff.php +++ b/WordPress/Sniffs/NamingConventions/ValidPostTypeSlugSniff.php @@ -9,222 +9,92 @@ namespace WordPressCS\WordPress\Sniffs\NamingConventions; -use PHP_CodeSniffer\Util\Tokens; -use PHPCSUtils\Tokens\Collections; -use PHPCSUtils\Utils\PassedParameters; -use PHPCSUtils\Utils\TextStrings; -use WordPressCS\WordPress\AbstractFunctionParameterSniff; +use WordPressCS\WordPress\AbstractValidSlugSniff; +use WordPressCS\WordPress\Helpers\WPReservedNamesHelper; /** * Validates post type names. * - * Checks the post type slug for invalid characters, long function names - * and reserved names. + * Checks post type slugs for the presence of invalid characters, excessive + * length, and reserved names. * * @link https://developer.wordpress.org/reference/functions/register_post_type/ * * @since 2.2.0 */ -final class ValidPostTypeSlugSniff extends AbstractFunctionParameterSniff { +final class ValidPostTypeSlugSniff extends AbstractValidSlugSniff { /** - * Max length of a post type name is limited by the SQL field. + * Retrieve function and parameter(s) pairs this sniff is looking for. * - * @since 2.2.0 + * @since 3.2.0 * - * @var int + * @return array> Function parameter(s) pairs. */ - const POST_TYPE_MAX_LENGTH = 20; + protected function get_target_functions() { + return array( + 'register_post_type' => array( 'post_type' ), + ); + } /** - * Regex to validate the characters that can be used as the post type slug. + * Retrieve the slug type. * - * @link https://developer.wordpress.org/reference/functions/register_post_type/ - * @since 2.2.0 - * @since 3.0.0 Renamed from `POST_TYPE_CHARACTER_WHITELIST` to `VALID_POST_TYPE_CHARACTERS`. + * @since 3.2.0 * - * @var string + * @return string The slug type. */ - const VALID_POST_TYPE_CHARACTERS = '/^[a-z0-9_-]+$/'; + protected function get_slug_type() { + return 'post type'; + } /** - * Array of functions that must be checked. + * Retrieve the plural slug type. * - * @since 2.2.0 + * @since 3.2.0 * - * @var array Key is function name, value irrelevant. + * @return string The plural slug type. */ - protected $target_functions = array( - 'register_post_type' => true, - ); + protected function get_slug_type_plural() { + return 'post types'; + } /** - * Array of reserved post type names which can not be used by themes and plugins. - * - * Source: {@link https://developer.wordpress.org/reference/functions/register_post_type/#reserved-post-types} + * Retrieve regex to validate the characters that can be used as the + * post type slug. * - * {@internal To be updated after every major release. Last updated for WordPress 6.5-RC3.} + * @since 3.2.0 * - * @since 2.2.0 + * @link https://developer.wordpress.org/reference/functions/register_post_type/ * - * @var array Key is reserved post type name, value irrelevant. + * @return string */ - protected $reserved_names = array( - 'action' => true, // Not a WP post type, but prevents other problems. - 'attachment' => true, - 'author' => true, // Not a WP post type, but prevents other problems. - 'custom_css' => true, - 'customize_changeset' => true, - 'nav_menu_item' => true, - 'oembed_cache' => true, - 'order' => true, // Not a WP post type, but prevents other problems. - 'page' => true, - 'post' => true, - 'revision' => true, - 'theme' => true, // Not a WP post type, but prevents other problems. - 'user_request' => true, - 'wp_block' => true, - 'wp_font_face' => true, - 'wp_font_family' => true, - 'wp_global_styles' => true, - 'wp_navigation' => true, - 'wp_template' => true, - 'wp_template_part' => true, - ); + protected function get_valid_characters() { + return '/^[a-z0-9_-]+$/'; + } /** - * All valid tokens for in the first parameter of register_post_type(). - * - * Set in `register()`. + * Retrieve max length of a post type name. * - * @since 2.2.0 - * - * @var array - */ - private $valid_tokens = array(); - - /** - * Returns an array of tokens this test wants to listen for. + * The length is limited by the SQL field. * - * @since 2.2.0 + * @since 3.2.0 * - * @return array + * @return int */ - public function register() { - $this->valid_tokens = Tokens::$textStringTokens + Tokens::$heredocTokens + Tokens::$emptyTokens; - return parent::register(); + protected function get_max_length() { + return 20; } /** - * Process the parameter of a matched function. - * - * Errors on invalid post type names when reserved keywords are used, - * the post type is too long, or contains invalid characters. - * - * @since 2.2.0 + * Retrieve the reserved post type names which can not be used + * by themes and plugins. * - * @param int $stackPtr The position of the current token in the stack. - * @param string $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched - * in lowercase. - * @param array $parameters Array with information about the parameters. + * @since 3.2.0 * - * @return void + * @return array */ - public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - $post_type_param = PassedParameters::getParameterFromStack( $parameters, 1, 'post_type' ); - if ( false === $post_type_param || '' === $post_type_param['clean'] ) { - // Error for using empty slug. - $this->phpcsFile->addError( - 'register_post_type() called without a post type slug. The slug must be a non-empty string.', - false === $post_type_param ? $stackPtr : $post_type_param['start'], - 'Empty' - ); - return; - } - - $string_start = $this->phpcsFile->findNext( Collections::textStringStartTokens(), $post_type_param['start'], ( $post_type_param['end'] + 1 ) ); - $string_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $post_type_param['start'], ( $post_type_param['end'] + 1 ) ); - - $has_invalid_tokens = $this->phpcsFile->findNext( $this->valid_tokens, $post_type_param['start'], ( $post_type_param['end'] + 1 ), true ); - if ( false !== $has_invalid_tokens || false === $string_pos ) { - // Check for non string based slug parameter (we cannot determine if this is valid). - $this->phpcsFile->addWarning( - 'The post type slug is not a string literal. It is not possible to automatically determine the validity of this slug. Found: %s.', - $stackPtr, - 'NotStringLiteral', - array( - $post_type_param['clean'], - ), - 3 - ); - return; - } - - $post_type = TextStrings::getCompleteTextString( $this->phpcsFile, $string_start ); - if ( isset( Tokens::$heredocTokens[ $this->tokens[ $string_start ]['code'] ] ) ) { - // Trim off potential indentation from PHP 7.3 flexible heredoc/nowdoc content. - $post_type = ltrim( $post_type ); - } - - $data = array( - $post_type, - ); - - // Warn for dynamic parts in the slug parameter. - if ( 'T_DOUBLE_QUOTED_STRING' === $this->tokens[ $string_pos ]['type'] - || ( 'T_HEREDOC' === $this->tokens[ $string_pos ]['type'] - && strpos( $this->tokens[ $string_pos ]['content'], '$' ) !== false ) - ) { - $this->phpcsFile->addWarning( - 'The post type slug may, or may not, get too long with dynamic contents and could contain invalid characters. Found: "%s".', - $string_pos, - 'PartiallyDynamic', - $data - ); - $post_type = TextStrings::stripEmbeds( $post_type ); - } - - if ( preg_match( self::VALID_POST_TYPE_CHARACTERS, $post_type ) === 0 ) { - // Error for invalid characters. - $this->phpcsFile->addError( - 'register_post_type() called with invalid post type "%s". Post type contains invalid characters. Only lowercase alphanumeric characters, dashes, and underscores are allowed.', - $string_pos, - 'InvalidCharacters', - $data - ); - } - - if ( isset( $this->reserved_names[ $post_type ] ) ) { - // Error for using reserved slug names. - $this->phpcsFile->addError( - 'register_post_type() called with reserved post type "%s". Reserved post types should not be used as they interfere with the functioning of WordPress itself.', - $string_pos, - 'Reserved', - $data - ); - } elseif ( stripos( $post_type, 'wp_' ) === 0 ) { - // Error for using reserved slug prefix. - $this->phpcsFile->addError( - 'The post type passed to register_post_type() uses a prefix reserved for WordPress itself. Found: "%s".', - $string_pos, - 'ReservedPrefix', - $data - ); - } - - // Error for slugs that are too long. - if ( strlen( $post_type ) > self::POST_TYPE_MAX_LENGTH ) { - $this->phpcsFile->addError( - 'A post type slug must not exceed %d characters. Found: "%s" (%d characters).', - $string_pos, - 'TooLong', - array( - self::POST_TYPE_MAX_LENGTH, - $post_type, - strlen( $post_type ), - ) - ); - } + protected function get_reserved_names() { + return WPReservedNamesHelper::get_post_types(); } } diff --git a/WordPress/Sniffs/NamingConventions/ValidTaxonomySlugSniff.php b/WordPress/Sniffs/NamingConventions/ValidTaxonomySlugSniff.php new file mode 100644 index 000000000..8b6819d87 --- /dev/null +++ b/WordPress/Sniffs/NamingConventions/ValidTaxonomySlugSniff.php @@ -0,0 +1,100 @@ +> Function parameter(s) pairs. + */ + protected function get_target_functions() { + return array( + 'register_taxonomy' => array( 'taxonomy' ), + ); + } + + /** + * Retrieve the slug type. + * + * @since 3.2.0 + * + * @return string The slug type. + */ + protected function get_slug_type() { + return 'taxonomy'; + } + + /** + * Retrieve the plural slug type. + * + * @since 3.2.0 + * + * @return string The plural slug type. + */ + protected function get_slug_type_plural() { + return 'taxonomies'; + } + + /** + * Retrieve regex to validate the characters that can be used as the + * taxonomy slug. + * + * @since 3.2.0 + * + * @link https://developer.wordpress.org/reference/functions/register_taxonomy/ + * + * @return string + */ + protected function get_valid_characters() { + return '/^[a-z0-9_-]+$/'; + } + + /** + * Retrieve max length of a taxonomy name. + * + * The length is limited by the SQL field. + * + * @since 3.2.0 + * + * @return int + */ + protected function get_max_length() { + return 32; + } + + /** + * Retrieve the reserved taxonomy names which can not be used + * by themes and plugins. + * + * @since 3.2.0 + * + * @return array + */ + protected function get_reserved_names() { + return WPReservedNamesHelper::get_terms(); + } +} diff --git a/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.1.inc b/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.1.inc index 5c0ed5729..c994383ea 100644 --- a/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.1.inc +++ b/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.1.inc @@ -49,7 +49,7 @@ EOD register_post_type( "my-own-post-type-too-long-{$i}" ); // 1x Error, Too long. 1x Warning, post type may or may not get too long with dynamic contents in the id. register_post_type( 'my/own/post/type/too/long', array() ); // Bad. Invalid chars: "/" and too long. -register_post_type( 'wp_block', array() ); // Bad. Must only error on reserved keyword, not invalid prefix. +register_post_type( 'wp_block', array() ); // Bad. Must only error on reserved name, not invalid prefix. // Test handling of more complex embedded variables and expressions. register_post_type("testing123-${(foo)}-test"); diff --git a/WordPress/Tests/NamingConventions/ValidTaxonomySlugUnitTest.1.inc b/WordPress/Tests/NamingConventions/ValidTaxonomySlugUnitTest.1.inc new file mode 100644 index 000000000..f647cfb63 --- /dev/null +++ b/WordPress/Tests/NamingConventions/ValidTaxonomySlugUnitTest.1.inc @@ -0,0 +1,70 @@ +get_taxonomy_id() ); // Non string literal. Warning with severity: 3 + +register_taxonomy( null, array() ); // Non string literal. Warning with severity: 3 +register_taxonomy( 1000, array() ); // Non string literal. Warning with severity: 3 + +register_taxonomy( 'wp_', array() ); // Bad. Reserved prefix. +register_taxonomy( 'wp_taxonomy', array() ); // Bad. Reserved prefix. + +register_taxonomy( '', array() ); // Bad. Empty taxonomy slug. +register_taxonomy( /*comment*/, array() ); // Bad. No taxonomy slug. + +register_taxonomy( 'taxonomy_1', array() ); // OK. + +register_taxonomy( <<warningSeverity = 3; + } + + /** + * Returns the lines where errors should occur. + * + * @param string $testFile The name of the file being tested. + * + * @return array Key is the line number, value is the number of expected errors. + */ + public function getErrorList( $testFile = '' ) { + switch ( $testFile ) { + case 'ValidTaxonomySlugUnitTest.1.inc': + return array( + 5 => 1, + 6 => 1, + 7 => 1, + 8 => 1, + 20 => 1, + 36 => 1, + 37 => 1, + 39 => 1, + 40 => 1, + 49 => 1, + 50 => 2, + 52 => 1, + 62 => 1, + 64 => 1, + ); + + case 'ValidTaxonomySlugUnitTest.2.inc': + // These tests will only yield reliable results when PHPCS is run on PHP 7.3 or higher. + if ( \PHP_VERSION_ID < 70300 ) { + return array(); + } + + return array( + 17 => 1, + ); + + default: + return array(); + } + } + + /** + * Returns the lines where warnings should occur. + * + * @param string $testFile The name of the file being tested. + * + * @return array Key is the line number, value is the number of expected warnings. + */ + public function getWarningList( $testFile = '' ) { + switch ( $testFile ) { + case 'ValidTaxonomySlugUnitTest.1.inc': + return array( + 24 => 1, + 27 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 31 => 1, + 33 => 1, + 34 => 1, + 45 => 1, + 49 => 1, + 55 => 1, + 56 => 1, + 67 => 1, + ); + + case 'ValidTaxonomySlugUnitTest.2.inc': + // These tests will only yield reliable results when PHPCS is run on PHP 7.3 or higher. + if ( \PHP_VERSION_ID < 70300 ) { + return array(); + } + + return array( + 7 => 1, + ); + + default: + return array(); + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a5f12e738..b50909b31 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -25,6 +25,7 @@ ./WordPress/AbstractClassRestrictionsSniff.php ./WordPress/AbstractFunctionParameterSniff.php ./WordPress/AbstractFunctionRestrictionsSniff.php + ./WordPress/AbstractValidSlugSniff.php ./WordPress/Sniffs/ ./WordPress/Helpers/