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 =