diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index b5a7f84ada..b45e37bceb 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -2244,6 +2244,24 @@ protected function tokenize($string) break; }//end for + // Handle live coding/parse errors elegantly. + // If the "?" is the last non-empty token in the file, we cannot draw a definitive conclusion, + // so tokenize as T_INLINE_THEN. + if ($i === $numTokens) { + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr at end of file changed from ? to T_INLINE_THEN".PHP_EOL; + } + + $newToken['code'] = T_INLINE_THEN; + $newToken['type'] = 'T_INLINE_THEN'; + + $insideInlineIf[] = $stackPtr; + + $finalTokens[$newStackPtr] = $newToken; + $newStackPtr++; + continue; + } + /* * This can still be a nullable type or a ternary. * Do additional checking. @@ -2253,6 +2271,11 @@ protected function tokenize($string) $lastSeenNonEmpty = null; for ($i = ($stackPtr - 1); $i >= 0; $i--) { + if (isset($tokens[$i]) === false) { + // Ignore skipped tokens (related to PHP 8+ slash/hash comment vs new line retokenization). + continue; + } + if (is_array($tokens[$i]) === true) { $tokenType = $tokens[$i][0]; } else { @@ -2268,7 +2291,7 @@ protected function tokenize($string) } if ($prevNonEmpty === null - && @isset(Tokens::$emptyTokens[$tokenType]) === false + && isset(Tokens::$emptyTokens[$tokenType]) === false ) { // Found the previous non-empty token. if ($tokenType === ':' || $tokenType === ',' || $tokenType === T_ATTRIBUTE_END) { @@ -2287,8 +2310,8 @@ protected function tokenize($string) if ($tokenType === T_FUNCTION || $tokenType === T_FN - || @isset(Tokens::$methodPrefixes[$tokenType]) === true - || @isset(Tokens::$scopeModifiers[$tokenType]) === true + || isset(Tokens::$methodPrefixes[$tokenType]) === true + || isset(Tokens::$scopeModifiers[$tokenType]) === true || $tokenType === T_VAR || $tokenType === T_READONLY ) { @@ -2311,7 +2334,7 @@ protected function tokenize($string) break; } - if (@isset(Tokens::$emptyTokens[$tokenType]) === false) { + if (isset(Tokens::$emptyTokens[$tokenType]) === false) { $lastSeenNonEmpty = $tokenType; } }//end for diff --git a/tests/Core/Tokenizers/PHP/NullableVsInlineThenParseErrorTest.inc b/tests/Core/Tokenizers/PHP/NullableVsInlineThenParseErrorTest.inc new file mode 100644 index 0000000000..fda129b16b --- /dev/null +++ b/tests/Core/Tokenizers/PHP/NullableVsInlineThenParseErrorTest.inc @@ -0,0 +1,9 @@ +phpcsFile->getTokens(); + $target = $this->getTargetToken('/* testLiveCoding */', [T_NULLABLE, T_INLINE_THEN]); + $tokenArray = $tokens[$target]; + + $this->assertSame(T_INLINE_THEN, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_INLINE_THEN (code)'); + $this->assertSame('T_INLINE_THEN', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_INLINE_THEN (type)'); + + }//end testInlineThenAtEndOfFile() + + +}//end class diff --git a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc index 398d2f6283..ac3c9f8b19 100644 --- a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc +++ b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc @@ -22,7 +22,11 @@ $closure = function ( /* testClosureParamTypeNullableInt */ ?Int $a, /* testClosureParamTypeNullableCallable */ - ? Callable $b + ? Callable $b, + /* testClosureParamTypeNullableStringWithAttributeAndSlashComment */ + #[AttributeForParam] + // This must be a slash or hash comment and the next line must **NOT** have any indentation for the PHP 8.5 deprecation notice (issue PHPCSStandards/PHP_CodeSniffer#1216) to occur. +?string $c /* testClosureReturnTypeNullableInt */ ) :?INT{}; diff --git a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php index 81d4b6c01c..0b7e5d5510 100644 --- a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php +++ b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php @@ -50,16 +50,17 @@ public function testNullable($testMarker) public static function dataNullable() { return [ - 'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'], - 'property declaration, private set' => ['/* testNullablePrivateSet */'], - 'property declaration, public and protected set' => ['/* testNullablePublicProtectedSet */'], - 'property declaration, final, no visibility' => ['/* testNullableFinalOnly */'], - 'property declaration, abstract, no visibility' => ['/* testNullableAbstractOnly */'], - - 'closure param type, nullable int' => ['/* testClosureParamTypeNullableInt */'], - 'closure param type, nullable callable' => ['/* testClosureParamTypeNullableCallable */'], - 'closure return type, nullable int' => ['/* testClosureReturnTypeNullableInt */'], - 'function return type, nullable callable' => ['/* testFunctionReturnTypeNullableCallable */'], + 'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'], + 'property declaration, private set' => ['/* testNullablePrivateSet */'], + 'property declaration, public and protected set' => ['/* testNullablePublicProtectedSet */'], + 'property declaration, final, no visibility' => ['/* testNullableFinalOnly */'], + 'property declaration, abstract, no visibility' => ['/* testNullableAbstractOnly */'], + + 'closure param type, nullable int' => ['/* testClosureParamTypeNullableInt */'], + 'closure param type, nullable callable' => ['/* testClosureParamTypeNullableCallable */'], + 'closure param type, nullable string with comment, issue #1216' => ['/* testClosureParamTypeNullableStringWithAttributeAndSlashComment */'], + 'closure return type, nullable int' => ['/* testClosureReturnTypeNullableInt */'], + 'function return type, nullable callable' => ['/* testFunctionReturnTypeNullableCallable */'], ]; }//end dataNullable()