PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / 2.7.8
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts v2.7.8
2.7.9 2.7.8 2.7.7 2.7.6 2.7.5 2.7.4 trunk 1.3 2.0.4 2.0.6 2.1.91 2.2.4 2.2.7 2.2.9 2.3.1 2.3.10 2.4.10 2.4.2 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.6.0 All 30 releases
← All changes | includes/class.code-validator.php +63 -360 trunk → 2.7.8 View file →
@@ -33,13 +33,14 @@
33 33 'message' => '',
34 34 ];
35 35 }
36 36
37 - $redeclaration_error = self::get_redeclaration_error( $snippet_code, $snippet_type );
38 - if ( null !== $redeclaration_error ) {
37 + $redeclaration = self::find_function_redeclaration( $snippet_code, $snippet_type );
38 + if ( null !== $redeclaration ) {
39 39 return [
40 40 'valid' => false,
41 - 'message' => $redeclaration_error->getMessage(),
41 + // translators: %1$d is the line number, %2$s is the fully qualified function name.
42 + 'message' => sprintf( __( 'Line %1$d: Cannot redeclare function %2$s(). Rename the function or guard its declaration with function_exists().', 'insert-php' ), $redeclaration['line'], $redeclaration['name'] ),
42 43 ];
43 44 }
44 45
45 46 $validation_errors = [];
@@ -117,47 +118,8 @@
117 118 ];
118 119 }
119 120
120 121 /**
121 - * Check executable snippet code for syntax errors without running it.
122 - *
123 - * The tokenizer parses the code in TOKEN_PARSE mode and never executes it.
124 - * Errors PHP raises only when the code runs, such as a call to an undefined
125 - * function, are left to the check that runs when the snippet is activated.
126 - * Without the tokenizer extension nothing is checked and the result says so.
127 - *
128 - * @param string $snippet_code Snippet code, without the opening tag for PHP snippets.
129 - * @param string $snippet_type Snippet type.
130 - * @return array{valid:bool,checked:bool,message:string} Check result.
131 - */
132 - public static function check_syntax( string $snippet_code, $snippet_type ): array {
133 - if ( ! self::can_tokenize() ) {
134 - return [
135 - 'valid' => true,
136 - 'checked' => false,
137 - 'message' => '',
138 - ];
139 - }
140 -
141 - try {
142 - token_get_all( self::get_source( $snippet_code, $snippet_type ), TOKEN_PARSE );
143 - } catch ( CompileError $e ) {
144 - return [
145 - 'valid' => false,
146 - 'checked' => true,
147 - // translators: %1$d is the line number, %2$s is the error message.
148 - 'message' => sprintf( __( 'Syntax error on line %1$d: %2$s', 'insert-php' ), $e->getLine(), $e->getMessage() ),
149 - ];
150 - }
151 -
152 - return [
153 - 'valid' => true,
154 - 'checked' => true,
155 - 'message' => '',
156 - ];
157 - }
158 -
159 - /**
160 122 * Find a top-level function declaration that would be redeclared by eval().
161 123 *
162 124 * PHP terminates the request when eval() declares an already-loaded function,
163 125 * so this conflict has to be detected before evaluating the snippet.
@@ -166,110 +128,14 @@
166 128 * @param string $snippet_type Snippet type.
167 129 * @return array{name:string,line:int}|null Conflict details, or null when none is found.
168 130 */
169 131 public static function find_function_redeclaration( $snippet_code, $snippet_type ) {
170 - $redeclaration = self::scan_redeclarations( (string) $snippet_code, $snippet_type, false );
171 - if ( null === $redeclaration ) {
172 - return null;
173 - }
132 + $source = WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type
133 + ? $snippet_code
134 + : '<?php ' . $snippet_code;
135 + $tokens = token_get_all( $source );
174 136
175 - return [
176 - 'name' => $redeclaration['name'],
177 - 'line' => $redeclaration['line'],
178 - ];
179 - }
180 -
181 - /**
182 - * Find a top-level function, class, interface, trait or enum that would be redeclared by eval().
183 - *
184 - * PHP terminates the request when eval() declares a name that is already in
185 - * use, so this conflict has to be detected before evaluating the snippet.
186 - *
187 - * @param string $snippet_code Snippet code.
188 - * @param string $snippet_type Snippet type.
189 - * @return array{kind:string,name:string,line:int}|null Conflict details, or null when none is found.
190 - */
191 - public static function find_redeclaration( string $snippet_code, $snippet_type ): ?array {
192 - return self::scan_redeclarations( $snippet_code, $snippet_type, true );
193 - }
194 -
195 - /**
196 - * Describe a redeclaration found by find_redeclaration().
197 - *
198 - * @param array{kind:string,name:string,line:int} $redeclaration Conflict details.
199 - * @return string
200 - */
201 - public static function get_redeclaration_message( array $redeclaration ): string {
202 - switch ( $redeclaration['kind'] ) {
203 - case 'class':
204 - // translators: %s is the fully qualified class name.
205 - return sprintf( __( 'Cannot declare class %s, because the name is already in use. Rename the class or guard its declaration with class_exists().', 'insert-php' ), $redeclaration['name'] );
206 - case 'interface':
207 - // translators: %s is the fully qualified interface name.
208 - return sprintf( __( 'Cannot declare interface %s, because the name is already in use. Rename the interface or guard its declaration with interface_exists().', 'insert-php' ), $redeclaration['name'] );
209 - case 'trait':
210 - // translators: %s is the fully qualified trait name.
211 - return sprintf( __( 'Cannot declare trait %s, because the name is already in use. Rename the trait or guard its declaration with trait_exists().', 'insert-php' ), $redeclaration['name'] );
212 - case 'enum':
213 - // translators: %s is the fully qualified enum name.
214 - return sprintf( __( 'Cannot declare enum %s, because the name is already in use. Rename the enum or guard its declaration with enum_exists().', 'insert-php' ), $redeclaration['name'] );
215 - default:
216 - // translators: %s is the fully qualified function name.
217 - return sprintf( __( 'Cannot redeclare function %s(). Rename the function or guard its declaration with function_exists().', 'insert-php' ), $redeclaration['name'] );
218 - }
219 - }
220 -
221 - /**
222 - * Build the error for a top-level declaration the snippet would redeclare.
223 - *
224 - * @param string $snippet_code Snippet code.
225 - * @param string $snippet_type Snippet type.
226 - * @return ErrorException|null Error carrying the snippet line, or null when none is found.
227 - */
228 - public static function get_redeclaration_error( string $snippet_code, $snippet_type ): ?ErrorException {
229 - $redeclaration = self::find_redeclaration( $snippet_code, $snippet_type );
230 - if ( null === $redeclaration ) {
231 - return null;
232 - }
233 -
234 - return new ErrorException(
235 - // translators: %1$d is the line number, %2$s is the error message.
236 - sprintf( __( 'Line %1$d: %2$s', 'insert-php' ), $redeclaration['line'], self::get_redeclaration_message( $redeclaration ) ),
237 - 0,
238 - E_COMPILE_ERROR,
239 - __FILE__,
240 - $redeclaration['line']
241 - );
242 - }
243 -
244 - /**
245 - * Find the first top-level declaration whose name is already in use.
246 - *
247 - * Top-level functions are bound when eval() compiles the code, so a conflict
248 - * always ends the request. Classes, interfaces, traits and enums are declared
249 - * when execution reaches them, so they are not reported after a return, exit,
250 - * throw or goto outside any function or class body, which may guard them.
251 - * Declarations inside braces are conditional and are skipped, even when the
252 - * condition is always true. Class-like declarations inside alternative-syntax
253 - * blocks, as in if ( ... ): ... endif;, are skipped too, while functions there
254 - * are still reported. Nothing is reported without the tokenizer extension.
255 - *
256 - * @param string $snippet_code Snippet code.
257 - * @param string $snippet_type Snippet type.
258 - * @param bool $include_classes Whether class-like declarations are checked.
259 - * @return array{kind:string,name:string,line:int}|null Conflict details, or null when none is found.
260 - */
261 - private static function scan_redeclarations( string $snippet_code, $snippet_type, bool $include_classes ): ?array {
262 - if ( ! self::can_tokenize() ) {
263 - return null;
264 - }
265 -
266 - $tokens = token_get_all( self::get_source( $snippet_code, $snippet_type ) );
267 - $declaration_kinds = self::get_declaration_kinds();
268 - $blocks = []; // One entry per open brace, true when it opens a function or class body.
269 - $opens_body = false;
270 - $alternative_depth = 0;
271 - $may_stop = false;
137 + $brace_depth = 0;
272 138 $namespace = '';
273 139 $namespace_depth = 0;
274 140 $reading_namespace = false;
275 141 $namespace_name = '';
@@ -275,116 +141,79 @@
275 141 $namespace_name = '';
276 142 $declared = [];
277 143
278 144 foreach ( $tokens as $index => $token ) {
279 - if ( ! is_array( $token ) ) {
280 - if ( $reading_namespace ) {
281 - $reading_namespace = false;
145 + if ( is_array( $token ) ) {
146 + if ( in_array( $token[0], [ T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES ], true ) ) {
147 + ++$brace_depth;
148 + continue;
149 + }
282 150
283 - if ( ';' === $token ) {
284 - $namespace = trim( $namespace_name, '\\' );
285 - $namespace_depth = 0;
286 - continue;
287 - }
151 + if ( T_NAMESPACE === $token[0] ) {
152 + $reading_namespace = true;
153 + $namespace_name = '';
154 + continue;
155 + }
288 156
289 - if ( '{' !== $token ) {
290 - $namespace_name = '';
291 - continue;
157 + if ( $reading_namespace ) {
158 + if ( self::is_namespace_name_token( $token[0] ) ) {
159 + $namespace_name .= $token[1];
160 + } elseif ( ! in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) ) {
161 + $reading_namespace = false;
162 + $namespace_name = '';
292 163 }
164 + continue;
165 + }
293 166
294 - $blocks[] = false;
295 - $namespace = trim( $namespace_name, '\\' );
296 - $namespace_depth = count( $blocks );
167 + if ( T_FUNCTION !== $token[0] || $brace_depth !== $namespace_depth ) {
297 168 continue;
298 169 }
299 170
300 - if ( '{' === $token ) {
301 - $blocks[] = $opens_body;
302 - $opens_body = false;
303 - } elseif ( '}' === $token ) {
304 - array_pop( $blocks );
305 - if ( $namespace_depth > count( $blocks ) ) {
306 - $namespace = '';
307 - $namespace_depth = 0;
308 - }
309 - } elseif ( ';' === $token ) {
310 - // Abstract and interface methods have no body.
311 - $opens_body = false;
171 + $function_name = self::get_function_name( $tokens, $index );
172 + if ( null === $function_name ) {
173 + continue;
312 174 }
313 - continue;
314 - }
315 175
316 - $token_id = $token[0];
176 + $qualified_name = $namespace ? $namespace . '\\' . $function_name : $function_name;
177 + $normalized_name = strtolower( $qualified_name );
317 178
318 - if ( in_array( $token_id, [ T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES ], true ) ) {
319 - $blocks[] = false;
320 - continue;
321 - }
179 + if ( isset( $declared[ $normalized_name ] ) || function_exists( $qualified_name ) ) {
180 + return [
181 + 'name' => $qualified_name,
182 + 'line' => (int) $token[2],
183 + ];
184 + }
322 185
323 - if ( T_NAMESPACE === $token_id ) {
324 - $reading_namespace = true;
325 - $namespace_name = '';
186 + $declared[ $normalized_name ] = true;
326 187 continue;
327 188 }
328 189
329 190 if ( $reading_namespace ) {
330 - if ( self::is_namespace_name_token( $token_id ) ) {
331 - $namespace_name .= $token[1];
332 - } elseif ( ! self::is_insignificant( $token ) ) {
191 + if ( ';' === $token ) {
192 + $namespace = trim( $namespace_name, '\\' );
193 + $namespace_depth = 0;
333 194 $reading_namespace = false;
195 + } elseif ( '{' === $token ) {
196 + ++$brace_depth;
197 + $namespace = trim( $namespace_name, '\\' );
198 + $namespace_depth = $brace_depth;
199 + $reading_namespace = false;
200 + } else {
201 + $reading_namespace = false;
334 202 $namespace_name = '';
335 203 }
336 204 continue;
337 205 }
338 206
339 - if ( in_array( $token_id, [ T_RETURN, T_EXIT, T_THROW, T_GOTO ], true ) ) {
340 - $may_stop = $may_stop || ! in_array( true, $blocks, true );
341 - continue;
342 - }
343 -
344 - if ( in_array( $token_id, [ T_IF, T_WHILE, T_FOR, T_FOREACH, T_SWITCH ], true ) ) {
345 - if ( self::opens_alternative_block( $tokens, $index ) ) {
346 - ++$alternative_depth;
207 + if ( '{' === $token ) {
208 + ++$brace_depth;
209 + } elseif ( '}' === $token ) {
210 + $brace_depth = max( 0, $brace_depth - 1 );
211 + if ( $namespace_depth > $brace_depth ) {
212 + $namespace = '';
213 + $namespace_depth = 0;
347 214 }
348 - continue;
349 215 }
350 -
351 - if ( in_array( $token_id, [ T_ENDIF, T_ENDWHILE, T_ENDFOR, T_ENDFOREACH, T_ENDSWITCH ], true ) ) {
352 - $alternative_depth = max( 0, $alternative_depth - 1 );
353 - continue;
354 - }
355 -
356 - if ( ! isset( $declaration_kinds[ $token_id ] ) || self::is_name_reference( $tokens, $index ) ) {
357 - continue;
358 - }
359 -
360 - // Named and anonymous functions and classes open their body at the next brace.
361 - $opens_body = true;
362 - $kind = $declaration_kinds[ $token_id ];
363 - $is_function = 'function' === $kind;
364 -
365 - if ( count( $blocks ) !== $namespace_depth || ( ! $is_function && ( ! $include_classes || $may_stop || $alternative_depth > 0 ) ) ) {
366 - continue;
367 - }
368 -
369 - $name = self::get_declared_name( $tokens, $index );
370 - if ( null === $name ) {
371 - continue;
372 - }
373 -
374 - $qualified_name = $namespace ? $namespace . '\\' . $name : $name;
375 - $symbol_key = ( $is_function ? 'function:' : 'class:' ) . strtolower( $qualified_name );
376 - $in_use = $is_function ? function_exists( $qualified_name ) : self::is_class_name_in_use( $qualified_name );
377 -
378 - if ( isset( $declared[ $symbol_key ] ) || $in_use ) {
379 - return [
380 - 'kind' => $kind,
381 - 'name' => $qualified_name,
382 - 'line' => (int) $token[2],
383 - ];
384 - }
385 -
386 - $declared[ $symbol_key ] = true;
387 216 }
388 217
389 218 return null;
390 219 }
@@ -389,134 +218,8 @@
389 218 return null;
390 219 }
391 220
392 221 /**
393 - * Check whether the tokenizer extension is available.
394 - *
395 - * @return bool
396 - */
397 - private static function can_tokenize(): bool {
398 - return function_exists( 'token_get_all' );
399 - }
400 -
401 - /**
402 - * Build the source the tokenizer reads: PHP snippets are stored without their opening tag.
403 - *
404 - * @param string $snippet_code Snippet code.
405 - * @param string $snippet_type Snippet type.
406 - * @return string
407 - */
408 - private static function get_source( string $snippet_code, $snippet_type ): string {
409 - return WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type ? $snippet_code : '<?php ' . $snippet_code;
410 - }
411 -
412 - /**
413 - * Map the declaration tokens to the kind of name they declare.
414 - *
415 - * @return array<int, string>
416 - */
417 - private static function get_declaration_kinds(): array {
418 - $kinds = [
419 - T_FUNCTION => 'function',
420 - T_CLASS => 'class',
421 - T_INTERFACE => 'interface',
422 - T_TRAIT => 'trait',
423 - ];
424 -
425 - if ( defined( 'T_ENUM' ) ) {
426 - $kinds[ constant( 'T_ENUM' ) ] = 'enum';
427 - }
428 -
429 - return $kinds;
430 - }
431 -
432 - /**
433 - * Check whether a class, interface, trait or enum with this name is already loaded.
434 - *
435 - * Autoloading is skipped: eval() only fails on names that are already declared.
436 - *
437 - * @param string $name Fully qualified name.
438 - * @return bool
439 - */
440 - private static function is_class_name_in_use( string $name ): bool {
441 - return class_exists( $name, false ) || interface_exists( $name, false ) || trait_exists( $name, false );
442 - }
443 -
444 - /**
445 - * Check whether a token is whitespace or a comment.
446 - *
447 - * @param array<int, int|string>|string $token Token.
448 - * @return bool
449 - */
450 - private static function is_insignificant( $token ): bool {
451 - return is_array( $token ) && in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true );
452 - }
453 -
454 - /**
455 - * Check whether a declaration keyword only refers to a name, as in Example::class or use function example;.
456 - *
457 - * @param array<int, array<int, int|string>|string> $tokens Token stream.
458 - * @param int $index Keyword token index.
459 - * @return bool
460 - */
461 - private static function is_name_reference( array $tokens, int $index ): bool {
462 - $preceding = [ T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_USE ];
463 -
464 - if ( defined( 'T_NULLSAFE_OBJECT_OPERATOR' ) ) {
465 - $preceding[] = constant( 'T_NULLSAFE_OBJECT_OPERATOR' );
466 - }
467 -
468 - for ( $previous = $index - 1; $previous >= 0; --$previous ) {
469 - if ( ! self::is_insignificant( $tokens[ $previous ] ) ) {
470 - return is_array( $tokens[ $previous ] ) && in_array( $tokens[ $previous ][0], $preceding, true );
471 - }
472 - }
473 -
474 - return false;
475 - }
476 -
477 - /**
478 - * Check whether a control structure uses the alternative syntax, as in if ( ... ): ... endif;.
479 - *
480 - * @param array<int, array<int, int|string>|string> $tokens Token stream.
481 - * @param int $index Control structure token index.
482 - * @return bool
483 - */
484 - private static function opens_alternative_block( array $tokens, int $index ): bool {
485 - $token_count = count( $tokens );
486 - $depth = 0;
487 -
488 - for ( $current = $index + 1; $current < $token_count; ++$current ) {
489 - $token = $tokens[ $current ];
490 -
491 - if ( self::is_insignificant( $token ) ) {
492 - continue;
493 - }
494 -
495 - if ( '(' === $token ) {
496 - ++$depth;
497 - continue;
498 - }
499 -
500 - if ( 0 === $depth ) {
501 - return false;
502 - }
503 -
504 - if ( ')' === $token && 0 === --$depth ) {
505 - for ( $next = $current + 1; $next < $token_count; ++$next ) {
506 - if ( ! self::is_insignificant( $tokens[ $next ] ) ) {
507 - return ':' === $tokens[ $next ];
508 - }
509 - }
510 -
511 - return false;
512 - }
513 - }
514 -
515 - return false;
516 - }
517 -
518 - /**
519 222 * Check whether a token can be part of a namespace name.
520 223 *
521 224 * @param int $token_id Token identifier.
522 225 * @return bool
@@ -533,22 +236,22 @@
533 236 return in_array( $token_id, $name_tokens, true );
534 237 }
535 238
536 239 /**
537 - * Get the name following a declaration token, ignoring whitespace and references.
240 + * Get the name following a function token, ignoring whitespace and references.
538 241 *
539 242 * @param array<int, array<int, int|string>|string> $tokens Token stream.
540 - * @param int $declaration_index Declaration token index.
541 - * @return string|null Null for closures, anonymous classes and other unnamed uses.
243 + * @param int $function_index Function token index.
244 + * @return string|null
542 245 */
543 - private static function get_declared_name( $tokens, $declaration_index ) {
246 + private static function get_function_name( $tokens, $function_index ) {
544 247 $token_count = count( $tokens );
545 248
546 - for ( $index = $declaration_index + 1; $index < $token_count; ++$index ) {
249 + for ( $index = $function_index + 1; $index < $token_count; ++$index ) {
547 250 $token = $tokens[ $index ];
548 251
549 252 if ( is_array( $token ) ) {
550 - if ( self::is_insignificant( $token ) || '&' === $token[1] ) {
253 + if ( in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) || '&' === $token[1] ) {
551 254 continue;
552 255 }
553 256
554 257 return T_STRING === $token[0] ? (string) $token[1] : null;