Skip to content

Commit e386fd1

Browse files
committed
Merge branch '3.x' into 4.x
2 parents 2e361b6 + e163dbf commit e386fd1

File tree

5 files changed

+89
-15
lines changed

5 files changed

+89
-15
lines changed

src/Tokenizers/PHP.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2172,6 +2172,24 @@ protected function tokenize(string $code)
21722172
break;
21732173
}
21742174

2175+
// Handle live coding/parse errors elegantly.
2176+
// If the "?" is the last non-empty token in the file, we cannot draw a definitive conclusion,
2177+
// so tokenize as T_INLINE_THEN.
2178+
if ($i === $numTokens) {
2179+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2180+
StatusWriter::write("* token $stackPtr at end of file changed from ? to T_INLINE_THEN", 2);
2181+
}
2182+
2183+
$newToken['code'] = T_INLINE_THEN;
2184+
$newToken['type'] = 'T_INLINE_THEN';
2185+
2186+
$insideInlineIf[] = $stackPtr;
2187+
2188+
$finalTokens[$newStackPtr] = $newToken;
2189+
$newStackPtr++;
2190+
continue;
2191+
}
2192+
21752193
/*
21762194
* This can still be a nullable type or a ternary.
21772195
* Do additional checking.
@@ -2181,6 +2199,11 @@ protected function tokenize(string $code)
21812199
$lastSeenNonEmpty = null;
21822200

21832201
for ($i = ($stackPtr - 1); $i >= 0; $i--) {
2202+
if (isset($tokens[$i]) === false) {
2203+
// Ignore skipped tokens (related to PHP 8+ slash/hash comment vs new line retokenization).
2204+
continue;
2205+
}
2206+
21842207
if (is_array($tokens[$i]) === true) {
21852208
$tokenType = $tokens[$i][0];
21862209
} else {
@@ -2196,7 +2219,7 @@ protected function tokenize(string $code)
21962219
}
21972220

21982221
if ($prevNonEmpty === null
2199-
&& @isset(Tokens::EMPTY_TOKENS[$tokenType]) === false
2222+
&& isset(Tokens::EMPTY_TOKENS[$tokenType]) === false
22002223
) {
22012224
// Found the previous non-empty token.
22022225
if ($tokenType === ':' || $tokenType === ',' || $tokenType === T_ATTRIBUTE_END) {
@@ -2215,8 +2238,8 @@ protected function tokenize(string $code)
22152238

22162239
if ($tokenType === T_FUNCTION
22172240
|| $tokenType === T_FN
2218-
|| @isset(Tokens::METHOD_MODIFIERS[$tokenType]) === true
2219-
|| @isset(Tokens::SCOPE_MODIFIERS[$tokenType]) === true
2241+
|| isset(Tokens::METHOD_MODIFIERS[$tokenType]) === true
2242+
|| isset(Tokens::SCOPE_MODIFIERS[$tokenType]) === true
22202243
|| $tokenType === T_VAR
22212244
|| $tokenType === T_READONLY
22222245
) {
@@ -2239,7 +2262,7 @@ protected function tokenize(string $code)
22392262
break;
22402263
}
22412264

2242-
if (@isset(Tokens::EMPTY_TOKENS[$tokenType]) === false) {
2265+
if (isset(Tokens::EMPTY_TOKENS[$tokenType]) === false) {
22432266
$lastSeenNonEmpty = $tokenType;
22442267
}
22452268
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
// This should be the only test in the file.
4+
// Ref: https://github.com/PHPCSStandards/PHP_CodeSniffer/issues/1216
5+
6+
/* testLiveCoding */
7+
$ternary = true
8+
# 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.
9+
? //comment.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
/**
3+
* Tests the retokenization of ? to T_NULLABLE or T_INLINE_THEN.
4+
*
5+
* @copyright 2025 PHPCSStandards and contributors
6+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
7+
*/
8+
9+
namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP;
10+
11+
use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase;
12+
13+
/**
14+
* Tests the retokenization of ? to T_NULLABLE or T_INLINE_THEN.
15+
*
16+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
17+
*/
18+
final class NullableVsInlineThenParseErrorTest extends AbstractTokenizerTestCase
19+
{
20+
21+
22+
/**
23+
* Verify that a "?" as the last functional token in a file (live coding) is tokenized as `T_INLINE_THEN`
24+
* as it cannot yet be determined what the token would be once the code is finalized.
25+
*
26+
* @return void
27+
*/
28+
public function testInlineThenAtEndOfFile()
29+
{
30+
$tokens = $this->phpcsFile->getTokens();
31+
$target = $this->getTargetToken('/* testLiveCoding */', [T_NULLABLE, T_INLINE_THEN]);
32+
$tokenArray = $tokens[$target];
33+
34+
$this->assertSame(T_INLINE_THEN, $tokenArray['code'], 'Token tokenized as ' . $tokenArray['type'] . ', not T_INLINE_THEN (code)');
35+
$this->assertSame('T_INLINE_THEN', $tokenArray['type'], 'Token tokenized as ' . $tokenArray['type'] . ', not T_INLINE_THEN (type)');
36+
}
37+
}

tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ $closure = function (
2222
/* testClosureParamTypeNullableInt */
2323
?Int $a,
2424
/* testClosureParamTypeNullableCallable */
25-
? Callable $b
25+
? Callable $b,
26+
/* testClosureParamTypeNullableStringWithAttributeAndSlashComment */
27+
#[AttributeForParam]
28+
// 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.
29+
?string $c
2630
/* testClosureReturnTypeNullableInt */
2731
) :?INT{};
2832

tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,17 @@ public function testNullable($testMarker)
4949
public static function dataNullable()
5050
{
5151
return [
52-
'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'],
53-
'property declaration, private set' => ['/* testNullablePrivateSet */'],
54-
'property declaration, public and protected set' => ['/* testNullablePublicProtectedSet */'],
55-
'property declaration, final, no visibility' => ['/* testNullableFinalOnly */'],
56-
'property declaration, abstract, no visibility' => ['/* testNullableAbstractOnly */'],
57-
58-
'closure param type, nullable int' => ['/* testClosureParamTypeNullableInt */'],
59-
'closure param type, nullable callable' => ['/* testClosureParamTypeNullableCallable */'],
60-
'closure return type, nullable int' => ['/* testClosureReturnTypeNullableInt */'],
61-
'function return type, nullable callable' => ['/* testFunctionReturnTypeNullableCallable */'],
52+
'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'],
53+
'property declaration, private set' => ['/* testNullablePrivateSet */'],
54+
'property declaration, public and protected set' => ['/* testNullablePublicProtectedSet */'],
55+
'property declaration, final, no visibility' => ['/* testNullableFinalOnly */'],
56+
'property declaration, abstract, no visibility' => ['/* testNullableAbstractOnly */'],
57+
58+
'closure param type, nullable int' => ['/* testClosureParamTypeNullableInt */'],
59+
'closure param type, nullable callable' => ['/* testClosureParamTypeNullableCallable */'],
60+
'closure param type, nullable string with comment, issue #1216' => ['/* testClosureParamTypeNullableStringWithAttributeAndSlashComment */'],
61+
'closure return type, nullable int' => ['/* testClosureReturnTypeNullableInt */'],
62+
'function return type, nullable callable' => ['/* testFunctionReturnTypeNullableCallable */'],
6263
];
6364
}
6465

0 commit comments

Comments
 (0)