diff --git a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php index d3bde6a40..910c139fd 100644 --- a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php +++ b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php @@ -110,21 +110,24 @@ final public function run( Check_Result $result ) { $_SERVER['argv'] = $this->parse_argv( $args, $defaults ); // Run PHPCS. + $php_codesniffer_error_handler = $this->register_php_codesniffer_error_handler(); try { ob_start(); - $runner = new Runner(); - $runner->runPHPCS(); + $this->run_php_codesniffer(); $reports = ob_get_clean(); } catch ( Exception $e ) { - $_SERVER['argv'] = $orig_cmd_args; + if ( ob_get_level() > 0 ) { + ob_end_clean(); + } throw $e; - } + } finally { + $this->restore_php_codesniffer_error_handler( $php_codesniffer_error_handler ); + // Reset installed_paths. + Config::setConfigData( 'installed_paths', $installed_paths, true ); - // Reset installed_paths. - Config::setConfigData( 'installed_paths', $installed_paths, true ); - - // Restore original arguments. - $_SERVER['argv'] = $orig_cmd_args; + // Restore original arguments. + $_SERVER['argv'] = $orig_cmd_args; + } // Parse the reports into data to add to the overall $result. $reports = json_decode( trim( $reports ), true ); @@ -154,6 +157,22 @@ final public function run( Check_Result $result ) { } } + /** + * Runs PHP_CodeSniffer. + * + * @since n.e.x.t + */ + protected function run_php_codesniffer() { + /* + * PHPStan cannot infer the class_exists() check performed by run(). + * + * @phpstan-ignore-next-line + */ + $runner = new Runner(); + /* @phpstan-ignore-next-line */ + $runner->runPHPCS(); + } + /** * Parse the command arguments. * @@ -229,6 +248,64 @@ private function get_argv_defaults( Check_Result $result ): array { return $defaults; } + /** + * Registers an error handler for known PHPCS notices. + * + * @since n.e.x.t + */ + private function register_php_codesniffer_error_handler() { + $previous_error_handler = null; + + $error_handler = static function ( $errno, $errstr, $errfile, $errline ) use ( &$previous_error_handler ) { + $normalized_file = wp_normalize_path( $errfile ); + + if ( + E_DEPRECATED === $errno && + false !== strpos( $errstr, 'auto_detect_line_endings is deprecated' ) && + false !== strpos( $normalized_file, 'vendor/squizlabs/php_codesniffer/src/Runner.php' ) + ) { + return true; + } + + /* + * PHPStan cannot infer the deferred assignment after the closure is created. + * + * @phpstan-ignore-next-line + */ + if ( is_callable( $previous_error_handler ) ) { + return (bool) call_user_func( $previous_error_handler, $errno, $errstr, $errfile, $errline ); + } + + return false; + }; + + $previous_error_handler = set_error_handler( $error_handler ); + + return $error_handler; + } + + /** + * Restores the error handler that was active before running PHP_CodeSniffer. + * + * PHP_CodeSniffer registers its own error handler while processing files. If + * processing throws, PHP_CodeSniffer does not restore that handler, so it must + * be removed before the temporary deprecation handler can be restored. + * + * @since n.e.x.t + * + * @param callable $php_codesniffer_error_handler The temporary error handler registered before running PHP_CodeSniffer. + */ + private function restore_php_codesniffer_error_handler( $php_codesniffer_error_handler ) { + + $current_error_handler = set_error_handler( $php_codesniffer_error_handler ); + restore_error_handler(); + + if ( $current_error_handler !== $php_codesniffer_error_handler ) { + restore_error_handler(); + } + + restore_error_handler(); + } /** * Resets \PHP_CodeSniffer\Config::$overriddenDefaults to prevent * incorrect results when running multiple checks. diff --git a/tests/phpunit/tests/Checker/Checks/Plugin_Review_PHPCS_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Plugin_Review_PHPCS_Check_Tests.php index 0e0779dba..e23c36672 100644 --- a/tests/phpunit/tests/Checker/Checks/Plugin_Review_PHPCS_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/Plugin_Review_PHPCS_Check_Tests.php @@ -161,4 +161,76 @@ public function test_run_without_errors() { $this->assertEquals( 0, $check_result->get_error_count() ); $this->assertEquals( 0, $check_result->get_warning_count() ); } + + public function test_run_suppresses_phpcs_auto_detect_line_endings_deprecation() { + + $plugin_review_phpcs_check = new Plugin_Review_PHPCS_Check(); + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-review-phpcs-without-errors/load.php' ); + $check_result = new Check_Result( $check_context ); + + $previous_error_handler = null; + + $previous_error_handler = set_error_handler( + static function ( $errno, $errstr, $errfile, $errline ) use ( &$previous_error_handler ) { + if ( E_DEPRECATED === $errno && false !== strpos( $errstr, 'auto_detect_line_endings is deprecated' ) ) { + throw new ErrorException( $errstr, 0, $errno, $errfile, $errline ); + } + + if ( is_callable( $previous_error_handler ) ) { + return (bool) call_user_func( $previous_error_handler, $errno, $errstr, $errfile, $errline ); + } + + return false; + } + ); + + try { + $plugin_review_phpcs_check->run( $check_result ); + } finally { + restore_error_handler(); + } + + $this->assertEquals( 0, $check_result->get_error_count() ); + $this->assertEquals( 0, $check_result->get_warning_count() ); + } + + public function test_run_restores_original_error_handler_when_phpcs_throws() { + $plugin_review_phpcs_check = new class() extends Plugin_Review_PHPCS_Check { + protected function run_php_codesniffer() { + set_error_handler( + static function () { + return false; + } + ); + throw new Exception( 'Test exception after PHP_CodeSniffer registers its error handler.' ); + } + }; + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-review-phpcs-without-errors/load.php' ); + $check_result = new Check_Result( $check_context ); + $original_error_handler = static function () { + return false; + }; + + set_error_handler( $original_error_handler ); + + try { + try { + $plugin_review_phpcs_check->run( $check_result ); + $this->fail( 'Expected PHP_CodeSniffer to throw an exception.' ); + } catch ( Exception $e ) { + $this->assertSame( 'Test exception after PHP_CodeSniffer registers its error handler.', $e->getMessage() ); + } + + $current_error_handler = set_error_handler( + static function () { + return false; + } + ); + restore_error_handler(); + + $this->assertSame( $original_error_handler, $current_error_handler ); + } finally { + restore_error_handler(); + } + } }