| @@ -15,8 +15,149 @@ | ||
| 15 | 15 | */ |
| 16 | 16 | class WINP_Code_Validator { |
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | + * Validate executable snippet code by evaluating it once. | |
| 20 | + * | |
| 21 | + * Runs the redeclaration preflight and then evaluates the code with output | |
| 22 | + * discarded, reporting syntax errors, warnings and uncaught errors. The code | |
| 23 | + * is executed in the current request, exactly as the snippet editor does. | |
| 24 | + * | |
| 25 | + * @param string $snippet_code Unslashed snippet code. | |
| 26 | + * @param string $snippet_type Snippet type. | |
| 27 | + * @return array{valid:bool,message:string} Validation result. | |
| 28 | + */ | |
| 29 | + public static function validate_code( $snippet_code, $snippet_type ) { | |
| 30 | + if ( empty( $snippet_code ) ) { | |
| 31 | + return [ | |
| 32 | + 'valid' => true, | |
| 33 | + 'message' => '', | |
| 34 | + ]; | |
| 35 | + } | |
| 36 | + | |
| 37 | + $redeclaration_error = self::get_redeclaration_error( $snippet_code, $snippet_type ); | |
| 38 | + if ( null !== $redeclaration_error ) { | |
| 39 | + return [ | |
| 40 | + 'valid' => false, | |
| 41 | + 'message' => $redeclaration_error->getMessage(), | |
| 42 | + ]; | |
| 43 | + } | |
| 44 | + | |
| 45 | + $validation_errors = []; | |
| 46 | + | |
| 47 | + // Set custom error handler to catch warnings and notices. | |
| 48 | + set_error_handler( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler | |
| 49 | + function ( $errno, $errstr, $errfile, $errline ) use ( &$validation_errors ) { | |
| 50 | + // Extract line number from eval'd code if present. | |
| 51 | + if ( strpos( $errfile, "eval()'d code" ) !== false ) { | |
| 52 | + // translators: %1$d is the line number, %2$s is the error message. | |
| 53 | + $validation_errors[] = sprintf( __( 'Line %1$d: %2$s', 'insert-php' ), $errline, $errstr ); | |
| 54 | + } else { | |
| 55 | + $validation_errors[] = $errstr; | |
| 56 | + } | |
| 57 | + return true; // Don't execute PHP internal error handler. | |
| 58 | + } | |
| 59 | + ); | |
| 60 | + | |
| 61 | + ob_start(); | |
| 62 | + | |
| 63 | + try { | |
| 64 | + $result = WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type | |
| 65 | + ? eval( '?> ' . $snippet_code . ' <?php ' ) | |
| 66 | + : eval( $snippet_code ); | |
| 67 | + } catch ( ParseError $e ) { | |
| 68 | + ob_end_clean(); | |
| 69 | + restore_error_handler(); | |
| 70 | + | |
| 71 | + return [ | |
| 72 | + 'valid' => false, | |
| 73 | + // translators: %1$d is the line number, %2$s is the error message. | |
| 74 | + 'message' => sprintf( __( 'Syntax error on line %1$d: %2$s', 'insert-php' ), $e->getLine(), $e->getMessage() ), | |
| 75 | + ]; | |
| 76 | + } catch ( Throwable $e ) { | |
| 77 | + ob_end_clean(); | |
| 78 | + restore_error_handler(); | |
| 79 | + | |
| 80 | + // For fatal errors in eval'd code, report the actual line number. | |
| 81 | + if ( strpos( $e->getFile(), "eval()'d code" ) !== false ) { | |
| 82 | + return [ | |
| 83 | + 'valid' => false, | |
| 84 | + // translators: %1$d is the line number, %2$s is the error message. | |
| 85 | + 'message' => sprintf( __( 'Error on line %1$d: %2$s', 'insert-php' ), $e->getLine(), $e->getMessage() ), | |
| 86 | + ]; | |
| 87 | + } | |
| 88 | + | |
| 89 | + return [ | |
| 90 | + 'valid' => false, | |
| 91 | + // translators: %s is the error message. | |
| 92 | + 'message' => sprintf( __( 'Error: %s', 'insert-php' ), $e->getMessage() ), | |
| 93 | + ]; | |
| 94 | + } | |
| 95 | + | |
| 96 | + // Discard any output (echo/print statements are normal for snippets). | |
| 97 | + ob_end_clean(); | |
| 98 | + restore_error_handler(); | |
| 99 | + | |
| 100 | + if ( ! empty( $validation_errors ) ) { | |
| 101 | + return [ | |
| 102 | + 'valid' => false, | |
| 103 | + 'message' => implode( '<br>', $validation_errors ), | |
| 104 | + ]; | |
| 105 | + } | |
| 106 | + | |
| 107 | + if ( false === $result ) { | |
| 108 | + return [ | |
| 109 | + 'valid' => false, | |
| 110 | + 'message' => __( 'The code contains syntax errors. Please review and fix them before saving.', 'insert-php' ), | |
| 111 | + ]; | |
| 112 | + } | |
| 113 | + | |
| 114 | + return [ | |
| 115 | + 'valid' => true, | |
| 116 | + 'message' => '', | |
| 117 | + ]; | |
| 118 | + } | |
| 119 | + | |
| 120 | + /** | |
| 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 | + /** | |
| 19 | 160 | * Find a top-level function declaration that would be redeclared by eval(). |
| 20 | 161 | * |
| 21 | 162 | * PHP terminates the request when eval() declares an already-loaded function, |
| 22 | 163 | * so this conflict has to be detected before evaluating the snippet. |
| @@ -25,14 +166,110 @@ | ||
| 25 | 166 | * @param string $snippet_type Snippet type. |
| 26 | 167 | * @return array{name:string,line:int}|null Conflict details, or null when none is found. |
| 27 | 168 | */ |
| 28 | 169 | public static function find_function_redeclaration( $snippet_code, $snippet_type ) { |
| 29 | - $source = WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type | |
| 30 | - ? $snippet_code | |
| 31 | - : '<?php ' . $snippet_code; | |
| 32 | - $tokens = token_get_all( $source ); | |
| 170 | + $redeclaration = self::scan_redeclarations( (string) $snippet_code, $snippet_type, false ); | |
| 171 | + if ( null === $redeclaration ) { | |
| 172 | + return null; | |
| 173 | + } | |
| 33 | 174 | |
| 34 | - $brace_depth = 0; | |
| 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; | |
| 35 | 272 | $namespace = ''; |
| 36 | 273 | $namespace_depth = 0; |
| 37 | 274 | $reading_namespace = false; |
| 38 | 275 | $namespace_name = ''; |
| @@ -38,79 +275,116 @@ | ||
| 38 | 275 | $namespace_name = ''; |
| 39 | 276 | $declared = []; |
| 40 | 277 | |
| 41 | 278 | foreach ( $tokens as $index => $token ) { |
| 42 | - if ( is_array( $token ) ) { | |
| 43 | - if ( in_array( $token[0], [ T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES ], true ) ) { | |
| 44 | - ++$brace_depth; | |
| 45 | - continue; | |
| 46 | - } | |
| 279 | + if ( ! is_array( $token ) ) { | |
| 280 | + if ( $reading_namespace ) { | |
| 281 | + $reading_namespace = false; | |
| 47 | 282 | |
| 48 | - if ( T_NAMESPACE === $token[0] ) { | |
| 49 | - $reading_namespace = true; | |
| 50 | - $namespace_name = ''; | |
| 51 | - continue; | |
| 52 | - } | |
| 283 | + if ( ';' === $token ) { | |
| 284 | + $namespace = trim( $namespace_name, '\\' ); | |
| 285 | + $namespace_depth = 0; | |
| 286 | + continue; | |
| 287 | + } | |
| 53 | 288 | |
| 54 | - if ( $reading_namespace ) { | |
| 55 | - if ( self::is_namespace_name_token( $token[0] ) ) { | |
| 56 | - $namespace_name .= $token[1]; | |
| 57 | - } elseif ( ! in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) ) { | |
| 58 | - $reading_namespace = false; | |
| 59 | - $namespace_name = ''; | |
| 289 | + if ( '{' !== $token ) { | |
| 290 | + $namespace_name = ''; | |
| 291 | + continue; | |
| 60 | 292 | } |
| 61 | - continue; | |
| 62 | - } | |
| 63 | 293 | |
| 64 | - if ( T_FUNCTION !== $token[0] || $brace_depth !== $namespace_depth ) { | |
| 294 | + $blocks[] = false; | |
| 295 | + $namespace = trim( $namespace_name, '\\' ); | |
| 296 | + $namespace_depth = count( $blocks ); | |
| 65 | 297 | continue; |
| 66 | 298 | } |
| 67 | 299 | |
| 68 | - $function_name = self::get_function_name( $tokens, $index ); | |
| 69 | - if ( null === $function_name ) { | |
| 70 | - continue; | |
| 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; | |
| 71 | 312 | } |
| 313 | + continue; | |
| 314 | + } | |
| 72 | 315 | |
| 73 | - $qualified_name = $namespace ? $namespace . '\\' . $function_name : $function_name; | |
| 74 | - $normalized_name = strtolower( $qualified_name ); | |
| 316 | + $token_id = $token[0]; | |
| 75 | 317 | |
| 76 | - if ( isset( $declared[ $normalized_name ] ) || function_exists( $qualified_name ) ) { | |
| 77 | - return [ | |
| 78 | - 'name' => $qualified_name, | |
| 79 | - 'line' => (int) $token[2], | |
| 80 | - ]; | |
| 81 | - } | |
| 318 | + if ( in_array( $token_id, [ T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES ], true ) ) { | |
| 319 | + $blocks[] = false; | |
| 320 | + continue; | |
| 321 | + } | |
| 82 | 322 | |
| 83 | - $declared[ $normalized_name ] = true; | |
| 323 | + if ( T_NAMESPACE === $token_id ) { | |
| 324 | + $reading_namespace = true; | |
| 325 | + $namespace_name = ''; | |
| 84 | 326 | continue; |
| 85 | 327 | } |
| 86 | 328 | |
| 87 | 329 | if ( $reading_namespace ) { |
| 88 | - if ( ';' === $token ) { | |
| 89 | - $namespace = trim( $namespace_name, '\\' ); | |
| 90 | - $namespace_depth = 0; | |
| 330 | + if ( self::is_namespace_name_token( $token_id ) ) { | |
| 331 | + $namespace_name .= $token[1]; | |
| 332 | + } elseif ( ! self::is_insignificant( $token ) ) { | |
| 91 | 333 | $reading_namespace = false; |
| 92 | - } elseif ( '{' === $token ) { | |
| 93 | - ++$brace_depth; | |
| 94 | - $namespace = trim( $namespace_name, '\\' ); | |
| 95 | - $namespace_depth = $brace_depth; | |
| 96 | - $reading_namespace = false; | |
| 97 | - } else { | |
| 98 | - $reading_namespace = false; | |
| 99 | 334 | $namespace_name = ''; |
| 100 | 335 | } |
| 101 | 336 | continue; |
| 102 | 337 | } |
| 103 | 338 | |
| 104 | - if ( '{' === $token ) { | |
| 105 | - ++$brace_depth; | |
| 106 | - } elseif ( '}' === $token ) { | |
| 107 | - $brace_depth = max( 0, $brace_depth - 1 ); | |
| 108 | - if ( $namespace_depth > $brace_depth ) { | |
| 109 | - $namespace = ''; | |
| 110 | - $namespace_depth = 0; | |
| 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; | |
| 111 | 347 | } |
| 348 | + continue; | |
| 112 | 349 | } |
| 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; | |
| 113 | 387 | } |
| 114 | 388 | |
| 115 | 389 | return null; |
| 116 | 390 | } |
| @@ -115,8 +389,134 @@ | ||
| 115 | 389 | return null; |
| 116 | 390 | } |
| 117 | 391 | |
| 118 | 392 | /** |
| 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 | + /** | |
| 119 | 519 | * Check whether a token can be part of a namespace name. |
| 120 | 520 | * |
| 121 | 521 | * @param int $token_id Token identifier. |
| 122 | 522 | * @return bool |
| @@ -133,22 +533,22 @@ | ||
| 133 | 533 | return in_array( $token_id, $name_tokens, true ); |
| 134 | 534 | } |
| 135 | 535 | |
| 136 | 536 | /** |
| 137 | - * Get the name following a function token, ignoring whitespace and references. | |
| 537 | + * Get the name following a declaration token, ignoring whitespace and references. | |
| 138 | 538 | * |
| 139 | 539 | * @param array<int, array<int, int|string>|string> $tokens Token stream. |
| 140 | - * @param int $function_index Function token index. | |
| 141 | - * @return string|null | |
| 540 | + * @param int $declaration_index Declaration token index. | |
| 541 | + * @return string|null Null for closures, anonymous classes and other unnamed uses. | |
| 142 | 542 | */ |
| 143 | - private static function get_function_name( $tokens, $function_index ) { | |
| 543 | + private static function get_declared_name( $tokens, $declaration_index ) { | |
| 144 | 544 | $token_count = count( $tokens ); |
| 145 | 545 | |
| 146 | - for ( $index = $function_index + 1; $index < $token_count; ++$index ) { | |
| 546 | + for ( $index = $declaration_index + 1; $index < $token_count; ++$index ) { | |
| 147 | 547 | $token = $tokens[ $index ]; |
| 148 | 548 | |
| 149 | 549 | if ( is_array( $token ) ) { |
| 150 | - if ( in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) || '&' === $token[1] ) { | |
| 550 | + if ( self::is_insignificant( $token ) || '&' === $token[1] ) { | |
| 151 | 551 | continue; |
| 152 | 552 | } |
| 153 | 553 | |
| 154 | 554 | return T_STRING === $token[0] ? (string) $token[1] : null; |