Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions src/Tokenizers/PHP.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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
) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

// This should be the only test in the file.
// Ref: https://github.com/PHPCSStandards/PHP_CodeSniffer/issues/1216

/* testLiveCoding */
$ternary = true
# This must be a slash or hash comment and the next line must **NOT** have any indentation for the PHP 8.5 deprecation notice to occur.
? //comment.
40 changes: 40 additions & 0 deletions tests/Core/Tokenizers/PHP/NullableVsInlineThenParseErrorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* Tests the retokenization of ? to T_NULLABLE or T_INLINE_THEN.
*
* @copyright 2025 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/

namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP;

use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase;

/**
* Tests the retokenization of ? to T_NULLABLE or T_INLINE_THEN.
*
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
*/
final class NullableVsInlineThenParseErrorTest extends AbstractTokenizerTestCase
{


/**
* Verify that a "?" as the last functional token in a file (live coding) is tokenized as `T_INLINE_THEN`
* as it cannot yet be determined what the token would be once the code is finalized.
*
* @return void
*/
public function testInlineThenAtEndOfFile()
{
$tokens = $this->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
6 changes: 5 additions & 1 deletion tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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{};

Expand Down
21 changes: 11 additions & 10 deletions tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down