diff --git a/composer.json b/composer.json index 6286fbc2b..71549fd86 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "composer/installers": "^v1.12.0 || ^2.2", "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "wp-coding-standards/wpcs": "^3.0.0", - "automattic/vipwpcs": "^3.0.0" + "automattic/vipwpcs": "^3.0.0", + "afragen/wordpress-plugin-readme-parser": "dev-master" }, "require-dev": { "wp-cli/extension-command": "^2.1", diff --git a/composer.lock b/composer.lock index e94ceecc8..cb943cda8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,55 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d6efb77e17ac9905f61dc15c9d198fba", + "content-hash": "5e14e6f00535c6f6789ee8f73a5ef33a", "packages": [ + { + "name": "afragen/wordpress-plugin-readme-parser", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/afragen/wordpress-plugin-readme-parser.git", + "reference": "c21c250a510972ac46658a315133ac8b91d60ba0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/afragen/wordpress-plugin-readme-parser/zipball/c21c250a510972ac46658a315133ac8b91d60ba0", + "reference": "c21c250a510972ac46658a315133ac8b91d60ba0", + "shasum": "" + }, + "require": { + "erusev/parsedown": "^1.7", + "php": ">=5.4" + }, + "default-branch": true, + "type": "library", + "autoload": { + "classmap": [ + "class-parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "WordPress.org", + "homepage": "https://meta.trac.wordpress.org/browser/sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme" + } + ], + "description": "A clone of the current WordPress.org Plugin Readme Parser, class-parser.php", + "keywords": [ + "parser", + "readme", + "wordpress" + ], + "support": { + "issues": "https://github.com/afragen/wordpress-plugin-readme-parser/issues", + "source": "https://github.com/afragen/wordpress-plugin-readme-parser/tree/master" + }, + "time": "2023-06-24T18:22:04+00:00" + }, { "name": "automattic/vipwpcs", "version": "3.0.0", @@ -289,6 +336,56 @@ }, "time": "2023-01-05T11:28:13+00:00" }, + { + "name": "erusev/parsedown", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/erusev/parsedown/issues", + "source": "https://github.com/erusev/parsedown/tree/1.7.x" + }, + "time": "2019-12-30T22:54:17+00:00" + }, { "name": "phpcsstandards/phpcsextra", "version": "1.1.2", @@ -4262,7 +4359,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "afragen/wordpress-plugin-readme-parser": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/includes/Checker/Checks/Plugin_Readme_Check.php b/includes/Checker/Checks/Plugin_Readme_Check.php index 4cd91def9..1363c9a47 100644 --- a/includes/Checker/Checks/Plugin_Readme_Check.php +++ b/includes/Checker/Checks/Plugin_Readme_Check.php @@ -12,6 +12,7 @@ use WordPress\Plugin_Check\Traits\Amend_Check_Result; use WordPress\Plugin_Check\Traits\Find_Readme; use WordPress\Plugin_Check\Traits\Stable_Check; +use WordPressdotorg\Plugin_Directory\Readme\Parser; /** * Check the plugins readme file and contents. @@ -69,14 +70,21 @@ protected function check_files( Check_Result $result, array $files ) { return; } + $readme_file = reset( $readme ); + + $parser = new Parser( $readme_file ); + // Check the readme file for default text. - $this->check_default_text( $result, $readme ); + $this->check_default_text( $result, $readme_file, $parser ); // Check the readme file for a valid license. - $this->check_license( $result, $readme ); + $this->check_license( $result, $readme_file, $parser ); // Check the readme file for a valid version. - $this->check_stable_tag( $result, $readme ); + $this->check_stable_tag( $result, $readme_file, $parser ); + + // Check the readme file for warnings. + $this->check_for_warnings( $result, $readme_file, $parser ); } /** @@ -84,27 +92,26 @@ protected function check_files( Check_Result $result, array $files ) { * * @since n.e.x.t * - * @param Check_Result $result The Check Result to amend. - * @param array $files Array of plugin files. + * @param Check_Result $result The Check Result to amend. + * @param string $readme_file Readme file. + * @param Parser $parser The Parser object. */ - private function check_default_text( Check_Result $result, array $files ) { - $default_text_patterns = array( - 'Here is a short description of the plugin.', - 'Tags: tag1', - 'Donate link: http://example.com/', - ); + private function check_default_text( Check_Result $result, string $readme_file, Parser $parser ) { + $short_description = $parser->short_description; + $tags = $parser->tags; + $donate_link = $parser->donate_link; - foreach ( $default_text_patterns as $pattern ) { - $file = self::file_str_contains( $files, $pattern ); - if ( $file ) { - $this->add_result_warning_for_file( - $result, - __( 'The readme appears to contain default text.', 'plugin-check' ), - 'default_readme_text', - $file - ); - break; - } + if ( + in_array( 'tag1', $tags, true ) + || str_contains( $short_description, 'Here is a short description of the plugin.' ) + || str_contains( $donate_link, '//example.com/' ) + ) { + $this->add_result_warning_for_file( + $result, + __( 'The readme appears to contain default text.', 'plugin-check' ), + 'default_readme_text', + $readme_file + ); } } @@ -113,25 +120,20 @@ private function check_default_text( Check_Result $result, array $files ) { * * @since n.e.x.t * - * @param Check_Result $result The Check Result to amend. - * @param array $files Array of plugin files. + * @param Check_Result $result The Check Result to amend. + * @param string $readme_file Readme file. + * @param Parser $parser The Parser object. */ - private function check_license( Check_Result $result, array $files ) { - $matches = array(); - // Get the license from the readme file. - $file = self::file_preg_match( '/(License:|License URI:)\s*(.+)*/i', $files, $matches ); - - if ( empty( $matches ) ) { - return; - } + private function check_license( Check_Result $result, string $readme_file, Parser $parser ) { + $license = $parser->license; // Test for a valid SPDX license identifier. - if ( ! preg_match( '/^([a-z0-9\-\+\.]+)(\sor\s([a-z0-9\-\+\.]+))*$/i', $matches[2] ) ) { + if ( ! empty( $license ) && ! preg_match( '/^([a-z0-9\-\+\.]+)(\sor\s([a-z0-9\-\+\.]+))*$/i', $license ) ) { $this->add_result_warning_for_file( $result, __( 'Your plugin has an invalid license declared. Please update your readme with a valid SPDX license identifier.', 'plugin-check' ), 'invalid_license', - $file + $readme_file ); } } @@ -141,25 +143,19 @@ private function check_license( Check_Result $result, array $files ) { * * @since n.e.x.t * - * @param Check_Result $result The Check Result to amend. - * @param array $files Array of plugin files. + * @param Check_Result $result The Check Result to amend. + * @param string $readme_file Readme file. + * @param Parser $parser The Parser object. */ - private function check_stable_tag( Check_Result $result, array $files ) { - $matches = array(); - // Get the Stable tag from readme file. - $file = self::file_preg_match( '/Stable tag:\s*([a-z0-9\.]+)/i', $files, $matches ); - if ( ! $file ) { - return; - } - - $stable_tag = isset( $matches[1] ) ? $matches[1] : ''; + private function check_stable_tag( Check_Result $result, string $readme_file, Parser $parser ) { + $stable_tag = $parser->stable_tag; if ( 'trunk' === $stable_tag ) { $this->add_result_error_for_file( $result, __( "It's recommended not to use 'Stable Tag: trunk'.", 'plugin-check' ), 'trunk_stable_tag', - $file + $readme_file ); } @@ -174,7 +170,51 @@ private function check_stable_tag( Check_Result $result, array $files ) { $result, __( 'The Stable Tag in your readme file does not match the version in your main plugin file.', 'plugin-check' ), 'stable_tag_mismatch', - $file + $readme_file + ); + } + } + + /** + * Checks the readme file warnings. + * + * @since n.e.x.t + * + * @param Check_Result $result The Check Result to amend. + * @param string $readme_file Readme file. + * @param Parser $parser The Parser object. + */ + private function check_for_warnings( Check_Result $result, string $readme_file, Parser $parser ) { + $warnings = $parser->warnings ? $parser->warnings : array(); + + $warning_keys = array_keys( $warnings ); + + $ignored_warnings = array( + 'contributor_ignored', + ); + + /** + * Filter the list of ignored readme parser warnings. + * + * @since n.e.x.t + * + * @param array $ignored_warnings Array of ignored warning keys. + * @param Parser $parser The Parser object. + */ + $ignored_warnings = (array) apply_filters( 'wp_plugin_check_ignored_readme_warnings', $ignored_warnings, $parser ); + + $warning_keys = array_diff( $warning_keys, $ignored_warnings ); + + if ( ! empty( $warning_keys ) ) { + $this->add_result_warning_for_file( + $result, + sprintf( + /* translators: list of warnings */ + esc_html__( 'The following readme parser warnings were detected: %s', 'plugin-check' ), + esc_html( implode( ', ', $warning_keys ) ) + ), + 'readme_parser_warnings', + $readme_file ); } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b8b9a4db3..0b56be811 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -19,5 +19,6 @@ parameters: message: '/^Function str_contains not found.$/' paths: - includes/Checker/Checks/Abstract_File_Check.php + - includes/Checker/Checks/Plugin_Readme_Check.php - includes/Traits/Find_Readme.php - includes/Traits/File_Editor_URL.php diff --git a/tests/phpunit/testdata/plugins/test-plugin-plugin-readme-parser-warnings/load.php b/tests/phpunit/testdata/plugins/test-plugin-plugin-readme-parser-warnings/load.php new file mode 100644 index 000000000..c2d375613 --- /dev/null +++ b/tests/phpunit/testdata/plugins/test-plugin-plugin-readme-parser-warnings/load.php @@ -0,0 +1,16 @@ +assertSame( 0, $check_result->get_error_count() ); $this->assertSame( 0, $check_result->get_warning_count() ); } + + public function test_run_with_errors_parser_warnings() { + $readme_check = new Plugin_Readme_Check(); + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-plugin-readme-parser-warnings/load.php' ); + $check_result = new Check_Result( $check_context ); + + $readme_check->run( $check_result ); + + $warnings = $check_result->get_warnings(); + + $this->assertNotEmpty( $warnings ); + $this->assertArrayHasKey( 'readme.txt', $warnings ); + $this->assertEquals( 1, $check_result->get_warning_count() ); + + // Check for parser warning. + $this->assertArrayHasKey( 0, $warnings['readme.txt'] ); + $this->assertArrayHasKey( 0, $warnings['readme.txt'][0] ); + $this->assertArrayHasKey( 'code', $warnings['readme.txt'][0][0][0] ); + $this->assertEquals( 'readme_parser_warnings', $warnings['readme.txt'][0][0][0]['code'] ); + } + + public function test_filter_readme_warnings_ignored() { + // Define custom ignore for testing. + $custom_ignores = array( + 'requires_php_header_ignored', + ); + + // Create a mock filter that will return our custom ignores. + $filter_name = 'wp_plugin_check_ignored_readme_warnings'; + add_filter( + $filter_name, + static function () use ( $custom_ignores ) { + return $custom_ignores; + } + ); + + $result = apply_filters( $filter_name, array() ); + + $this->assertEquals( $custom_ignores, $result ); + + // Remove the filter to avoid interfering with other tests. + remove_filter( + $filter_name, + static function () use ( $custom_ignores ) { + return $custom_ignores; + } + ); + } + + public function test_filter_wp_plugin_check_ignored_readme_warnings_will_return_no_error() { + // Define custom ignore for testing. + $custom_ignores = array( + 'requires_php_header_ignored', + 'contributor_ignored', + ); + + // Create a mock filter that will return our custom ignores. + $filter_name = 'wp_plugin_check_ignored_readme_warnings'; + add_filter( + $filter_name, + static function () use ( $custom_ignores ) { + return $custom_ignores; + } + ); + + $readme_check = new Plugin_Readme_Check(); + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-plugin-readme-parser-warnings/load.php' ); + $check_result = new Check_Result( $check_context ); + + $readme_check->run( $check_result ); + + $errors = $check_result->get_errors(); + $warnings = $check_result->get_warnings(); + + $this->assertEmpty( $errors ); + $this->assertEmpty( $warnings ); + $this->assertSame( 0, $check_result->get_error_count() ); + $this->assertSame( 0, $check_result->get_warning_count() ); + + // Remove the filter to avoid interfering with other tests. + remove_filter( + $filter_name, + static function () use ( $custom_ignores ) { + return $custom_ignores; + } + ); + } }