| @@ -33,14 +33,13 @@ | ||
| 33 | 33 | 'message' => '', |
| 34 | 34 | ]; |
| 35 | 35 | } |
| 36 | 36 | |
| 37 | - $redeclaration = self::find_function_redeclaration( $snippet_code, $snippet_type ); | |
| 38 | - if ( null !== $redeclaration ) { | |
| 37 | + $redeclaration_error = self::get_redeclaration_error( $snippet_code, $snippet_type ); | |
| 38 | + if ( null !== $redeclaration_error ) { | |
| 39 | 39 | return [ |
| 40 | 40 | 'valid' => false, |
| 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'] ), | |
| 41 | + 'message' => $redeclaration_error->getMessage(), | |
| 43 | 42 | ]; |
| 44 | 43 | } |
| 45 | 44 | |
| 46 | 45 | $validation_errors = []; |
| @@ -118,8 +117,47 @@ | ||
| 118 | 117 | ]; |
| 119 | 118 | } |
| 120 | 119 | |
| 121 | 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 | + /** | |
| 122 | 160 | * Find a top-level function declaration that would be redeclared by eval(). |
| 123 | 161 | * |
| 124 | 162 | * PHP terminates the request when eval() declares an already-loaded function, |
| 125 | 163 | * so this conflict has to be detected before evaluating the snippet. |
| @@ -128,14 +166,110 @@ | ||
| 128 | 166 | * @param string $snippet_type Snippet type. |
| 129 | 167 | * @return array{name:string,line:int}|null Conflict details, or null when none is found. |
| 130 | 168 | */ |
| 131 | 169 | public static function find_function_redeclaration( $snippet_code, $snippet_type ) { |
| 132 | - $source = WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type | |
| 133 | - ? $snippet_code | |
| 134 | - : '<?php ' . $snippet_code; | |
| 135 | - $tokens = token_get_all( $source ); | |
| 170 | + $redeclaration = self::scan_redeclarations( (string) $snippet_code, $snippet_type, false ); | |
| 171 | + if ( null === $redeclaration ) { | |
| 172 | + return null; | |
| 173 | + } | |
| 136 | 174 | |
| 137 | - $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; | |
| 138 | 272 | $namespace = ''; |
| 139 | 273 | $namespace_depth = 0; |
| 140 | 274 | $reading_namespace = false; |
| 141 | 275 | $namespace_name = ''; |
| @@ -141,79 +275,116 @@ | ||
| 141 | 275 | $namespace_name = ''; |
| 142 | 276 | $declared = []; |
| 143 | 277 | |
| 144 | 278 | foreach ( $tokens as $index => $token ) { |
| 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 | - } | |
| 279 | + if ( ! is_array( $token ) ) { | |
| 280 | + if ( $reading_namespace ) { | |
| 281 | + $reading_namespace = false; | |
| 150 | 282 | |
| 151 | - if ( T_NAMESPACE === $token[0] ) { | |
| 152 | - $reading_namespace = true; | |
| 153 | - $namespace_name = ''; | |
| 154 | - continue; | |
| 155 | - } | |
| 283 | + if ( ';' === $token ) { | |
| 284 | + $namespace = trim( $namespace_name, '\\' ); | |
| 285 | + $namespace_depth = 0; | |
| 286 | + continue; | |
| 287 | + } | |
| 156 | 288 | |
| 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 = ''; | |
| 289 | + if ( '{' !== $token ) { | |
| 290 | + $namespace_name = ''; | |
| 291 | + continue; | |
| 163 | 292 | } |
| 164 | - continue; | |
| 165 | - } | |
| 166 | 293 | |
| 167 | - if ( T_FUNCTION !== $token[0] || $brace_depth !== $namespace_depth ) { | |
| 294 | + $blocks[] = false; | |
| 295 | + $namespace = trim( $namespace_name, '\\' ); | |
| 296 | + $namespace_depth = count( $blocks ); | |
| 168 | 297 | continue; |
| 169 | 298 | } |
| 170 | 299 | |
| 171 | - $function_name = self::get_function_name( $tokens, $index ); | |
| 172 | - if ( null === $function_name ) { | |
| 173 | - 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; | |
| 174 | 312 | } |
| 313 | + continue; | |
| 314 | + } | |
| 175 | 315 | |
| 176 | - $qualified_name = $namespace ? $namespace . '\\' . $function_name : $function_name; | |
| 177 | - $normalized_name = strtolower( $qualified_name ); | |
| 316 | + $token_id = $token[0]; | |
| 178 | 317 | |
| 179 | - if ( isset( $declared[ $normalized_name ] ) || function_exists( $qualified_name ) ) { | |
| 180 | - return [ | |
| 181 | - 'name' => $qualified_name, | |
| 182 | - 'line' => (int) $token[2], | |
| 183 | - ]; | |
| 184 | - } | |
| 318 | + if ( in_array( $token_id, [ T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES ], true ) ) { | |
| 319 | + $blocks[] = false; | |
| 320 | + continue; | |
| 321 | + } | |
| 185 | 322 | |
| 186 | - $declared[ $normalized_name ] = true; | |
| 323 | + if ( T_NAMESPACE === $token_id ) { | |
| 324 | + $reading_namespace = true; | |
| 325 | + $namespace_name = ''; | |
| 187 | 326 | continue; |
| 188 | 327 | } |
| 189 | 328 | |
| 190 | 329 | if ( $reading_namespace ) { |
| 191 | - if ( ';' === $token ) { | |
| 192 | - $namespace = trim( $namespace_name, '\\' ); | |
| 193 | - $namespace_depth = 0; | |
| 330 | + if ( self::is_namespace_name_token( $token_id ) ) { | |
| 331 | + $namespace_name .= $token[1]; | |
| 332 | + } elseif ( ! self::is_insignificant( $token ) ) { | |
| 194 | 333 | $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; | |
| 202 | 334 | $namespace_name = ''; |
| 203 | 335 | } |
| 204 | 336 | continue; |
| 205 | 337 | } |
| 206 | 338 | |
| 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; | |
| 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; | |
| 214 | 347 | } |
| 348 | + continue; | |
| 215 | 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; | |
| 216 | 387 | } |
| 217 | 388 | |
| 218 | 389 | return null; |
| 219 | 390 | } |
| @@ -218,8 +389,134 @@ | ||
| 218 | 389 | return null; |
| 219 | 390 | } |
| 220 | 391 | |
| 221 | 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 | + /** | |
| 222 | 519 | * Check whether a token can be part of a namespace name. |
| 223 | 520 | * |
| 224 | 521 | * @param int $token_id Token identifier. |
| 225 | 522 | * @return bool |
| @@ -236,22 +533,22 @@ | ||
| 236 | 533 | return in_array( $token_id, $name_tokens, true ); |
| 237 | 534 | } |
| 238 | 535 | |
| 239 | 536 | /** |
| 240 | - * Get the name following a function token, ignoring whitespace and references. | |
| 537 | + * Get the name following a declaration token, ignoring whitespace and references. | |
| 241 | 538 | * |
| 242 | 539 | * @param array<int, array<int, int|string>|string> $tokens Token stream. |
| 243 | - * @param int $function_index Function token index. | |
| 244 | - * @return string|null | |
| 540 | + * @param int $declaration_index Declaration token index. | |
| 541 | + * @return string|null Null for closures, anonymous classes and other unnamed uses. | |
| 245 | 542 | */ |
| 246 | - private static function get_function_name( $tokens, $function_index ) { | |
| 543 | + private static function get_declared_name( $tokens, $declaration_index ) { | |
| 247 | 544 | $token_count = count( $tokens ); |
| 248 | 545 | |
| 249 | - for ( $index = $function_index + 1; $index < $token_count; ++$index ) { | |
| 546 | + for ( $index = $declaration_index + 1; $index < $token_count; ++$index ) { | |
| 250 | 547 | $token = $tokens[ $index ]; |
| 251 | 548 | |
| 252 | 549 | if ( is_array( $token ) ) { |
| 253 | - if ( in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) || '&' === $token[1] ) { | |
| 550 | + if ( self::is_insignificant( $token ) || '&' === $token[1] ) { | |
| 254 | 551 | continue; |
| 255 | 552 | } |
| 256 | 553 | |
| 257 | 554 | return T_STRING === $token[0] ? (string) $token[1] : null; |