PluginProbe
Plugin Check (PCP) / 0.2.1
Plugin Check (PCP) v0.2.1
2.1.0 trunk 0.1 0.2.0 0.2.1 0.2.2 0.2.3 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 1.9.0 2.0.0 ci-artifacts
plugin-check / checks / phpcs.php

phpcs.php in Plugin Check (PCP) 0.2.1, at checks/phpcs.php

246 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WordPressdotorg\Plugin_Check\Checks;
3 use const WordPressdotorg\Plugin_Check\{ PLUGIN_DIR, HAS_VENDOR };
4 use WordPressdotorg\Plugin_Check\{Error, Guideline_Violation, Message, Notice, Warning};
5 use WordPressdotorg\Plugin_Check\PHPCS;
6
7 include PLUGIN_DIR . '/inc/class-php-cli.php';
8 include PLUGIN_DIR . '/inc/class-phpcs.php';
9
10 class PHPCS_Checks extends Check_Base {
11
12 const NOTICE_TYPES = [
13 // This should be an Error, but this is triggered for all variablse with SQL which isn't always a problem.
14 //'WordPress.DB.PreparedSQL.InterpolatedNotPrepared' => Warning::class,
15 ];
16
17 public function check_against_phpcs() {
18 if ( ! HAS_VENDOR ) {
19 return new Notice(
20 'phpcs_not_tested',
21 __( 'PHP Code Sniffer rulesets have not been tested, as the vendor directory is missing. Perhaps you need to run <code>`composer install`</code>.', 'plugin-check' )
22 );
23 }
24
25 return $this->run_phpcs_standard(
26 __DIR__ . '/phpcs/plugin-check.xml'
27 );
28 }
29
30 public function check_against_phpcs_review() {
31 if ( ! HAS_VENDOR ) {
32 return new Notice(
33 'phpcs_not_tested',
34 __( 'PHP Code Sniffer rulesets have not been tested, as the vendor directory is missing. Perhaps you need to run <code>`composer install`</code>.', 'plugin-check' )
35 );
36 }
37
38 return $this->run_phpcs_standard(
39 __DIR__ . '/phpcs/plugin-check-needs-review.xml'
40 );
41 }
42
43 protected function run_phpcs_standard( string $standard, array $args = [] ) {
44 $phpcs = new PHPCS();
45 $phpcs->set_standard( $standard );
46
47 $args = wp_parse_args(
48 $args,
49 array(
50 'extensions' => 'php', // Only check php files.
51 's' => true, // Show the name of the sniff triggering a violation.
52 // --ignore-annotations
53 )
54 );
55
56 $report = $phpcs->run_json_report(
57 $this->path,
58 $args,
59 'array'
60 );
61
62 if ( is_wp_error( $report ) ) {
63 return new Error(
64 $report->get_error_code(),
65 $report->get_error_message()
66 );
67 }
68
69 // If no response, either malformed output or PHP encountered an error.
70 if ( ! $report || empty( $report['files'] ) ) {
71 return false;
72 }
73
74 return $this->phpcs_result_to_warnings( $report );
75 }
76
77 protected function phpcs_result_to_warnings( $result ) {
78 $return = [];
79
80 array_walk( $result['files'], function( $output, $filename ) use( &$return ) {
81 if ( ! $output['messages'] ) {
82 return;
83 }
84
85 // Ignore the column, and just use the Error + Line number.
86 $messages = [];
87 foreach ( $output['messages'] as &$message ) {
88 $messages[ $message['source'] . ':' . $message['line'] ] = $message;
89 }
90
91 foreach ( $messages as $message ) {
92 switch( strtoupper( $message['type'] ) ) {
93 case 'ERROR':
94 $notice_class = Error::class;
95 break;
96 case 'WARNING':
97 $notice_class = Warning::class;
98 break;
99 case 'INFO':
100 case 'NOTICE':
101 $notice_class = Notice::class;
102 break;
103 default:
104 $notice_class = Message::class;
105 }
106
107 // Allow for individual notices to be overridden.
108 if ( isset( self::NOTICE_TYPES[ $message['source'] ] ) ) {
109 $notice_class = self::NOTICE_TYPES[ $message['source'] ];
110 }
111
112 $source_code = esc_html( trim( file( $this->path . '/' . $filename )[ $message['line'] - 1 ] ) );
113
114 if ( current_user_can( 'edit_plugins' ) ) {
115 $edit_link = sprintf(
116 '<a href="%1$s" title="%2$s" aria-label="%2$s" target="_blank">%3$s</a>',
117 $this->get_file_editor_url( $filename, $message['line'] ),
118 sprintf(
119 /* translators: %s is the path to a plugin file. */
120 esc_attr__( 'View %s in the plugin file editor.', 'plugin-check' ),
121 $this->slug . '/' . $filename
122 ),
123 esc_html__( 'View in code editor', 'plugin-check' )
124 );
125 }
126
127 $return[] = new $notice_class(
128 $message['source'],
129 sprintf(
130 /* translators: 1: Type of Error 2: Line 3: File 4: Message 5: Code Example 6: Edit Link */
131 __( '%1$s Line %2$d of file %3$s.<br>%4$s.<br>%5$s%6$s', 'plugin-check' ),
132 "<strong>{$message['source']}</strong>",
133 $message['line'],
134 $filename,
135 rtrim( $message['message'], '.' ),
136 "<pre class='wp-plugin-check-code'><code>{$source_code}</code></pre>",
137 $edit_link ?? ''
138 )
139 );
140 }
141 } );
142
143 return $return;
144 }
145
146 /**
147 * Get the URL for opening the plugin file in an external editor.
148 *
149 * @since 0.2.1
150 *
151 * @param array $filename Source of PHPCS error.
152 * @param array $line Line number of PHPCS error.
153 *
154 * @return string|null File editor URL or null if not available.
155 */
156 private function get_file_editor_url( $filename, $line ) {
157 if ( ! isset( $filename, $line ) ) {
158 return null;
159 }
160
161 $edit_url = null;
162
163 /**
164 * Filters the template for the URL for linking to an external editor to open a file for editing.
165 *
166 * Users of IDEs that support opening files in via web protocols can use this filter to override
167 * the edit link to result in their editor opening rather than the plugin editor.
168 *
169 * The initial filtered value is null, requiring extension plugins to supply the URL template
170 * string themselves. If no template string is provided, links to the plugin editors will
171 * be provided if available. For example, for an extension plugin to cause file edit links to
172 * open in an IDE, the following filters can be used:
173 *
174 * # PhpStorm
175 * add_filter( 'plugin_check_validation_error_source_file_editor_url_template', function () {
176 * return 'phpstorm://open?file={{file}}&line={{line}}';
177 * } );
178 *
179 * # VS Code
180 * add_filter( 'plugin_check_validation_error_source_file_editor_url_template', function () {
181 * return 'vscode://file/{{file}}:{{line}}';
182 * } );
183 *
184 * For a template to be considered, the string '{{file}}' must be present in the filtered value.
185 *
186 * @since 0.2.1
187 *
188 * @param string|null $editor_url_template Editor URL template.
189 */
190 $editor_url_template = apply_filters( 'plugin_check_validation_error_source_file_editor_url_template', null );
191
192 // Supply the file path to the editor template.
193 if ( null !== $editor_url_template && str_contains( $editor_url_template, '{{file}}' ) ) {
194 $file_path = WP_PLUGIN_DIR . '/' . $this->slug;
195 if ( $this->slug !== $filename ) {
196 $file_path .= '/' . $filename;
197 }
198
199 if ( $file_path && file_exists( $file_path ) ) {
200 /**
201 * Filters the file path to be opened in an external editor for a given PHPCS error source.
202 *
203 * This is useful to map the file path from inside of a Docker container or VM to the host machine.
204 *
205 * @since 0.2.1
206 *
207 * @param string|null $editor_url_template Editor URL template.
208 * @param array $source Source information.
209 */
210 $file_path = apply_filters( 'plugin_check_validation_error_source_file_path', $file_path, array( $this->slug, $filename, $line) );
211 if ( $file_path ) {
212 $edit_url = str_replace(
213 [
214 '{{file}}',
215 '{{line}}',
216 ],
217 [
218 rawurlencode( $file_path ),
219 rawurlencode( $line ),
220 ],
221 $editor_url_template
222 );
223 }
224
225 }
226 }
227
228 // Fall back to using the plugin editor if no external editor is offered.
229 if ( ! $edit_url ) {
230 $plugin_data = get_plugins( '/' . $this->slug );
231
232 return esc_url(
233 add_query_arg(
234 [
235 'plugin' => rawurlencode( $this->slug . '/' . array_key_first( $plugin_data ) ),
236 'file' => rawurlencode( $this->slug . '/' . $filename ),
237 'line' => rawurlencode( $line ),
238 ],
239 admin_url( 'plugin-editor.php' )
240 )
241 );
242 }
243
244 return $edit_url;
245 }
246 }