diff --git a/.gitattributes b/.gitattributes index 329ab71..e676aad 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,7 @@ # /.cache export-ignore /.github export-ignore +/config export-ignore /grunt export-ignore /tests export-ignore .eslintrc export-ignore diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml index 8fa40f4..09b4281 100644 --- a/.github/workflows/cs.yml +++ b/.github/workflows/cs.yml @@ -63,7 +63,7 @@ jobs: # @link https://github.com/staabm/annotate-pull-request-from-checkstyle/ - name: Check PHP code style id: phpcs - run: composer check-cs -- --no-cache --report-full --report-checkstyle=./phpcs-report.xml + run: composer check-cs-warnings -- --no-cache --report-full --report-checkstyle=./phpcs-report.xml - name: Show PHPCS results in PR if: ${{ always() && steps.phpcs.outcome == 'failure' }} diff --git a/composer.json b/composer.json index 11bc0f4..4ef5218 100644 --- a/composer.json +++ b/composer.json @@ -58,6 +58,9 @@ ] }, "autoload-dev": { + "classmap": [ + "config/" + ], "psr-4": { "Yoast\\WP\\ACF\\Tests\\": "tests/" }, @@ -78,28 +81,50 @@ "lint": [ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --show-deprecated --exclude vendor --exclude node_modules --exclude .git" ], - "test": [ - "@php ./vendor/phpunit/phpunit/phpunit --no-coverage" + "cs": [ + "Yoast\\WP\\ACF\\Config\\Composer\\Actions::check_coding_standards" ], - "coverage": [ - "@php ./vendor/phpunit/phpunit/phpunit" + "check-cs-thresholds": [ + "@putenv YOASTCS_THRESHOLD_ERRORS=0", + "@putenv YOASTCS_THRESHOLD_WARNINGS=0", + "Yoast\\WP\\ACF\\Config\\Composer\\Actions::check_cs_thresholds" ], "check-cs": [ - "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" + "@check-cs-warnings -n" ], "check-cs-errors": [ "@check-cs" ], + "check-cs-warnings": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" + ], + "check-staged-cs": [ + "@check-cs-warnings --filter=GitStaged" + ], + "check-branch-cs": [ + "Yoast\\WP\\ACF\\Config\\Composer\\Actions::check_branch_cs" + ], "fix-cs": [ "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" + ], + "test": [ + "@php ./vendor/phpunit/phpunit/phpunit --no-coverage" + ], + "coverage": [ + "@php ./vendor/phpunit/phpunit/phpunit" ] }, "scripts-descriptions": { "lint": "Check the PHP files for parse errors.", - "test": "Run the unit tests without code coverage.", - "coverage": "Run the unit tests with code coverage.", - "check-cs": "Check the PHP files for code style violations and best practices.", + "cs": "See a menu with the code style checking script options.", + "check-cs-thresholds": "Check the PHP files for code style violations and best practices and verify the number of issues does not exceed predefined thresholds.", + "check-cs": "Check the PHP files for code style violations and best practices, ignoring warnings.", "check-cs-errors": "Alias for check-cs script.", - "fix-cs": "Auto-fix code style violations in the PHP files." + "check-cs-warnings": "Check the PHP files for code style violations and best practices, including warnings.", + "check-staged-cs": "Check the staged PHP files for code style violations and best practices.", + "check-branch-cs": "Check the PHP files changed in the current branch for code style violations and best practices.", + "fix-cs": "Auto-fix code style violations in the PHP files.", + "test": "Run the unit tests without code coverage.", + "coverage": "Run the unit tests with code coverage." } } diff --git a/config/composer/actions.php b/config/composer/actions.php new file mode 100644 index 0000000..e897d08 --- /dev/null +++ b/config/composer/actions.php @@ -0,0 +1,189 @@ +getIO(); + + $choices = [ + '1' => [ + 'label' => 'Check staged files for coding standard warnings & errors.', + 'command' => 'check-staged-cs', + ], + '2' => [ + 'label' => 'Check current branch\'s changed files for coding standard warnings & errors.', + 'command' => 'check-branch-cs', + ], + '3' => [ + 'label' => 'Check for all coding standard errors.', + 'command' => 'check-cs', + ], + '4' => [ + 'label' => 'Check for all coding standard warnings & errors.', + 'command' => 'check-cs-warnings', + ], + '5' => [ + 'label' => 'Fix auto-fixable coding standards.', + 'command' => 'fix-cs', + ], + '6' => [ + 'label' => 'Verify coding standard violations are below thresholds.', + 'command' => 'check-cs-thresholds', + ], + ]; + + $args = $event->getArguments(); + if ( empty( $args ) ) { + foreach ( $choices as $choice => $data ) { + $io->write( \sprintf( '%d. %s', $choice, $data['label'] ) ); + } + + $choice = $io->ask( 'What do you want to do? ' ); + } + else { + $choice = $args[0]; + } + + if ( isset( $choices[ $choice ] ) ) { + $event_dispatcher = $event->getComposer()->getEventDispatcher(); + $event_dispatcher->dispatchScript( $choices[ $choice ]['command'] ); + } + else { + $io->write( 'Unknown choice.' ); + } + } + + /** + * Runs PHPCS on the files changed in the current branch. + * + * Used by the composer check-branch-cs command. + * + * @codeCoverageIgnore + * + * @param Event $event Composer event that triggered this script. + * + * @return void + */ + public static function check_branch_cs( Event $event ) { + $branch = 'develop'; + + $args = $event->getArguments(); + if ( ! empty( $args ) ) { + $branch = $args[0]; + } + + exit( self::check_cs_for_changed_files( $branch ) ); + } + + /** + * Runs PHPCS on changed files compared to some git reference. + * + * @codeCoverageIgnore + * + * @param string $compare The git reference. + * + * @return int Exit code passed from the coding standards check. + */ + private static function check_cs_for_changed_files( $compare ) { + \exec( 'git diff --name-only --diff-filter=d ' . \escapeshellarg( $compare ), $files ); + + $php_files = self::filter_files( $files, '.php' ); + if ( empty( $php_files ) ) { + echo 'No files to compare! Exiting.' . \PHP_EOL; + + return 0; + } + + /* + * In CI, generate both the normal report as well as the checkstyle report. + * The normal report will be shown in the actions output and ensures human readable (and colorized!) results there. + * The checkstyle report is used to show the results inline in the GitHub code view. + */ + $extra_args = ( \getenv( 'CI' ) === false ) ? '' : ' --colors --no-cache --report-full --report-checkstyle=./phpcs-report.xml'; + $command = \sprintf( + 'composer check-cs-warnings -- %s %s', + \implode( ' ', \array_map( 'escapeshellarg', $php_files ) ), + $extra_args + ); + \system( $command, $exit_code ); + + return $exit_code; + } + + /** + * Checks if the CS errors and warnings are below or at thresholds. + * + * @return void + */ + public static function check_cs_thresholds() { + $in_ci = \getenv( 'CI' ); + + echo 'Running coding standards checks, this may take some time.', \PHP_EOL; + + $command = 'composer check-cs-warnings -- -mq --report="YoastCS\\Yoast\\Reports\\Threshold"'; + if ( $in_ci !== false ) { + // Always show the results in CI in color. + $command .= ' --colors'; + } + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Non-WP context, this is fine. + @\exec( $command, $phpcs_output, $return ); + + $phpcs_output = \implode( \PHP_EOL, $phpcs_output ); + echo $phpcs_output; + + $above_threshold = true; + if ( \strpos( $phpcs_output, 'Coding standards checks have passed!' ) !== false ) { + $above_threshold = false; + } + + /* + * Don't run the branch check in CI/GH Actions as it prevents the errors from being shown inline. + * The GH Actions script will run this via a separate script step. + */ + if ( $above_threshold === true && $in_ci === false ) { + echo \PHP_EOL; + echo 'Running check-branch-cs.', \PHP_EOL; + echo 'This might show problems on untouched lines. Focus on the lines you\'ve changed first.', \PHP_EOL; + echo \PHP_EOL; + + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Non-WP context, this is fine. + @\passthru( 'composer check-branch-cs' ); + } + + exit( ( $above_threshold === true || $return > 2 ) ? $return : 0 ); + } + + /** + * Filter files on extension. + * + * @param array $files List of files. + * @param string $extension Extension to filter on. + * + * @return array Filtered list of files. + */ + private static function filter_files( array $files, string $extension ): array { + return \array_filter( + $files, + static function ( $file ) use ( $extension ) { + return \substr( $file, ( 0 - \strlen( $extension ) ) ) === $extension; + } + ); + } +}