From f450396b2ab46a93ee7969db69e654dc079ddaff Mon Sep 17 00:00:00 2001 From: Gustavo Bordoni Date: Fri, 24 Nov 2023 01:51:17 -0500 Subject: [PATCH] Partially copy the version 1.0.0 usage of PHPCS in more Envs --- checks/phpcs.php | 76 +++++++++++++-- inc/class-phpcs-runner.php | 195 +++++++++++++++++++++++++++++++++++++ readme.txt | 2 + 3 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 inc/class-phpcs-runner.php diff --git a/checks/phpcs.php b/checks/phpcs.php index 61debecb8..5948c1a3b 100644 --- a/checks/phpcs.php +++ b/checks/phpcs.php @@ -3,14 +3,12 @@ use const WordPressdotorg\Plugin_Check\{ PLUGIN_DIR, HAS_VENDOR }; use WordPressdotorg\Plugin_Check\{Error, Guideline_Violation, Message, Notice, Warning}; use WordPressdotorg\Plugin_Check\PHPCS; - -include PLUGIN_DIR . '/inc/class-php-cli.php'; -include PLUGIN_DIR . '/inc/class-phpcs.php'; +use WordPressdotorg\Plugin_Check\PHPCS_Runner; class PHPCS_Checks extends Check_Base { const NOTICE_TYPES = [ - // This should be an Error, but this is triggered for all variablse with SQL which isn't always a problem. + // This should be an Error, but this is triggered for all variables with SQL which isn't always a problem. //'WordPress.DB.PreparedSQL.InterpolatedNotPrepared' => Warning::class, ]; @@ -27,7 +25,31 @@ public function check_against_phpcs() { ); } + /** + * Attempts to load Codesniffer and return a status if it's safe to use the runner. + * + * @since 0.2.2 + * + * @return bool + */ + protected function load_codesniffer_runner(): bool { + if ( class_exists( '\PHP_CodeSniffer\Runner' ) ) { + return true; + } + + // Include the PHPCS autoloader. + $autoloader = PLUGIN_DIR . '/vendor/squizlabs/php_codesniffer/autoload.php'; + + if ( file_exists( $autoloader ) ) { + include_once $autoloader; + } + + return class_exists( '\PHP_CodeSniffer\Runner' ); + } + public function check_against_phpcs_review() { + return null; + if ( ! HAS_VENDOR ) { return new Notice( 'phpcs_not_tested', @@ -35,12 +57,15 @@ public function check_against_phpcs_review() { ); } - return $this->run_phpcs_standard( + return $this->run_cli_phpcs_standard( __DIR__ . '/phpcs/plugin-check-needs-review.xml' ); } - protected function run_phpcs_standard( string $standard, array $args = [] ) { + protected function run_cli_phpcs_standard( string $standard, array $args = [] ) { + include_once PLUGIN_DIR . '/inc/class-php-cli.php'; + include_once PLUGIN_DIR . '/inc/class-phpcs.php'; + $phpcs = new PHPCS(); $phpcs->set_standard( $standard ); @@ -74,6 +99,32 @@ protected function run_phpcs_standard( string $standard, array $args = [] ) { return $this->phpcs_result_to_warnings( $report ); } + protected function run_phpcs_standard( string $standard, array $args = [] ) { + include_once PLUGIN_DIR . '/inc/class-phpcs-runner.php'; + + if ( ! $this->load_codesniffer_runner() ) { + return new Notice( + 'phpcs_runner_not_found', + esc_html__( 'PHP Code Sniffer rulesets have not been tested, as the Code Sniffer Runner class is missing.', 'plugin-check' ) + ); + } + + $phpcs = new PHPCS_Runner(); + $phpcs->set_path( $this->path ); + $phpcs->set_standard( $standard ); + + $results = $phpcs->run(); + + if ( is_wp_error( $results ) ) { + return new Error( + $results->get_error_code(), + $results->get_error_message() + ); + } + + return $this->phpcs_result_to_warnings( $results ); + } + protected function phpcs_result_to_warnings( $result ) { $return = []; @@ -109,7 +160,9 @@ protected function phpcs_result_to_warnings( $result ) { $notice_class = self::NOTICE_TYPES[ $message['source'] ]; } - $source_code = esc_html( trim( file( $this->path . '/' . $filename )[ $message['line'] - 1 ] ) ); + $file_path = dirname( $this->path ) . '/' . $filename; + + $source_code = esc_html( trim( file( $file_path )[ $message['line'] - 1 ] ) ); if ( current_user_can( 'edit_plugins' ) ) { $edit_link = sprintf( @@ -128,7 +181,7 @@ protected function phpcs_result_to_warnings( $result ) { $message['source'], sprintf( /* translators: 1: Type of Error 2: Line 3: File 4: Message 5: Code Example 6: Edit Link */ - __( '%1$s Line %2$d of file %3$s.
%4$s.
%5$s%6$s', 'plugin-check' ), + __( '%1$s

Line %2$d of file %3$s.
%4$s.
%5$s%6$s', 'plugin-check' ), "{$message['source']}", $message['line'], $filename, @@ -153,7 +206,7 @@ protected function phpcs_result_to_warnings( $result ) { * * @return string|null File editor URL or null if not available. */ - private function get_file_editor_url( $filename, $line ) { + protected function get_file_editor_url( $filename, $line ) { if ( ! isset( $filename, $line ) ) { return null; } @@ -228,12 +281,15 @@ private function get_file_editor_url( $filename, $line ) { // Fall back to using the plugin editor if no external editor is offered. if ( ! $edit_url ) { $plugin_data = get_plugins( '/' . $this->slug ); + if ( ! str_starts_with( $filename, $this->slug ) ) { + $filename = $this->slug . '/' . $filename; + } return esc_url( add_query_arg( [ 'plugin' => rawurlencode( $this->slug . '/' . array_key_first( $plugin_data ) ), - 'file' => rawurlencode( $this->slug . '/' . $filename ), + 'file' => rawurlencode( $filename ), 'line' => rawurlencode( $line ), ], admin_url( 'plugin-editor.php' ) diff --git a/inc/class-phpcs-runner.php b/inc/class-phpcs-runner.php new file mode 100644 index 000000000..26dd799eb --- /dev/null +++ b/inc/class-phpcs-runner.php @@ -0,0 +1,195 @@ + true, + 'extensions' => true, + 'sniffs' => true, + 'exclude' => true, //phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude + ]; + + /** + * Plugin path which will be used for the runner. + * + * @since 0.2.2 + * + * @var string + */ + protected $path; + + /** + * Which standard file we will use. + * + * @since 0.2.2 + * + * @var string + */ + protected $standard; + + public function set_path( $path ) { + $this->path = $path; + } + + public function get_path() { + return $this->path; + } + + public function set_standard( $standard ) { + $this->standard = $standard; + } + + public function get_standard() { + return $this->standard; + } + + /** + * Returns an associative array of arguments to pass to PHPCS. + * + * @since 0.2.2 + * + * @return array { + * An associative array of PHPCS CLI arguments. Can include one or more of the following options. + * + * @type string $standard The name or path to the coding standard to check against. + * @type string $extensions A comma separated list of file extensions to check against. + * @type string $sniffs A comma separated list of sniff codes to include from checks. + * @type string $exclude A comma separated list of sniff codes to exclude from checks. + * } + */ + protected function get_args() { + return [ + 'extensions' => 'php', + 'standard' => $this->get_standard(), + ]; + } + + /** + * Amends the given result by running the check on the associated plugin. + * + * @since 0.2.2 + * + * @return string|WP_Error|null + */ + public function run() { + // Backup the original command line arguments. + $orig_cmd_args = $_SERVER['argv']; + + // Create the default arguments for PHPCS. + $defaults = [ + '', + $this->get_path(), + '--report=Json', + '--report-width=9999', + ]; + + // Set the check arguments for PHPCS. + $_SERVER['argv'] = $this->parse_argv( $this->get_args(), $defaults ); + + // Reset PHP_CodeSniffer config. + $this->reset_php_codesniffer_config(); + + // Run PHPCS. + try { + ob_start(); + $runner = new \PHP_CodeSniffer\Runner(); + $runner->runPHPCS(); + $reports = ob_get_clean(); + } catch ( \Exception $e ) { + return new \WP_Error( + 'plugin_check_no_php_files_found', + esc_html__( 'PHP Code Sniffer cannot be completed.', 'plugin-check' ), + [ + 'error_code' => $e->getCode(), + 'error_message' => $e->getMessage(), + ] + ); + } + + // 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 ); + + if ( empty( $reports['files'] ) ) { + return new \WP_Error( + 'plugin_check_no_php_files_found', + esc_html__( 'Cannot find any PHP file to check, make sure your plugin contains PHP files.', 'plugin-check' ) + ); + } + + $base_dir = trailingslashit( basename( $this->get_path() ) ); + $plugin_path = $this->get_path(); + + $files_paths = array_map( static function( $file_path ) use ( $base_dir, $plugin_path ) { + return str_replace( $plugin_path, $base_dir, $file_path ); + }, array_keys( $reports['files'] ) ); + $files_values = array_values( $reports['files'] ); + + $reports['files'] = array_combine( $files_paths, $files_values ); + + return $reports; + } + + /** + * Parse the command arguments. + * + * @since 0.2.2 + * + * @param array $argv An array of arguments to pass. + * @param array $defaults An array of default arguments. + * + * @return array An indexed array of PHPCS CLI arguments. + */ + protected function parse_argv( $argv, $defaults ) { + // Only accept allowed PHPCS arguments from check arguments array. + $check_args = array_intersect_key( $argv, $this->allowed_args ); + + // Format check arguments for PHPCS. + foreach ( $check_args as $key => $value ) { + $defaults[] = "--{$key}=$value"; + } + + return $defaults; + } + + /** + * Resets \PHP_CodeSniffer\Config::$overriddenDefaults to prevent + * incorrect results when running multiple checks. + * + * @since 0.2.2 + */ + protected function reset_php_codesniffer_config() { + if ( class_exists( '\PHP_CodeSniffer\Config' ) ) { + /* + * PHPStan ignore reason: PHPStan raised an issue because we can't + * use class in ReflectionClass. + * + * @phpstan-ignore-next-line + */ + $reflected_phpcs_config = new \ReflectionClass( '\PHP_CodeSniffer\Config' ); + $overridden_defaults = $reflected_phpcs_config->getProperty( 'overriddenDefaults' ); + $overridden_defaults->setAccessible( true ); + $overridden_defaults->setValue( [] ); + $overridden_defaults->setAccessible( false ); + } + } +} \ No newline at end of file diff --git a/readme.txt b/readme.txt index f782bc73e..a7040e7c1 100644 --- a/readme.txt +++ b/readme.txt @@ -44,6 +44,8 @@ This plugin checker is not perfect, and never will be. It is only a tool to help = [0.2.2] 2023-11-XX = +* Enhancement - Include support for Windows Servers. +* Enhancement - Avoid using PHP CLI directly, which enables plugin developers to use PCP in a variety of new environments. * Fix - Remove extra period on the end of the sentence for Phar warning. Props @bordoni, @pixolin. [#275](https://github.com/10up/plugin-check/pull/265) = [0.2.1] 2023-09-22 =