Skip to content

Commit 55bb12f

Browse files
committed
PHP 8.1 | Tokenizer/PHP: fix nullable type tokenization for readonly properties
The `?` in a type for a `readonly` property declared without visibility, would be tokenized as `T_INLINE_THEN`, not `T_NULLABLE`. Fixed now. Includes perfunctory test. Realistically, a lot more tests are needed for the `T_NULLABLE` vs `T_INLINE_THEN` tokenization, but at least, this is a start and covers the current change.
1 parent 615e25a commit 55bb12f

File tree

3 files changed

+116
-0
lines changed

3 files changed

+116
-0
lines changed

src/Tokenizers/PHP.php

+7
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,12 @@ protected function tokenize($string)
14941494
];
14951495
$newStackPtr++;
14961496

1497+
// Also modify the original token stack so that
1498+
// future checks (like looking for T_NULLABLE) can
1499+
// detect the T_READONLY token more easily.
1500+
$tokens[$stackPtr][0] = T_READONLY;
1501+
$token[0] = T_READONLY;
1502+
14971503
if (PHP_CODESNIFFER_VERBOSITY > 1 && $type !== T_READONLY) {
14981504
echo "\t\t* token $stackPtr changed from $type to T_READONLY".PHP_EOL;
14991505
}
@@ -2142,6 +2148,7 @@ protected function tokenize($string)
21422148
|| $tokenType === T_FN
21432149
|| isset(Tokens::$methodPrefixes[$tokenType]) === true
21442150
|| $tokenType === T_VAR
2151+
|| $tokenType === T_READONLY
21452152
) {
21462153
if (PHP_CODESNIFFER_VERBOSITY > 1) {
21472154
echo "\t\t* token $stackPtr changed from ? to T_NULLABLE".PHP_EOL;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
class Nullable
4+
{
5+
/* testNullableReadonlyOnly */
6+
readonly ?int $prop;
7+
}
8+
9+
class InlineThen
10+
{
11+
/* testInlineThenInPropertyDefaultValue */
12+
public int $prop = self::SOMECONT ? PHP_CONST ? OTHER_CONST;
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 NullableVsInlineThenTest extends AbstractTokenizerTestCase
19+
{
20+
21+
22+
/**
23+
* Test that the ? at the start of type declarations is retokenized to T_NULLABLE.
24+
*
25+
* @param string $testMarker The comment which prefaces the target token in the test file.
26+
*
27+
* @dataProvider dataNullable
28+
*
29+
* @return void
30+
*/
31+
public function testNullable($testMarker)
32+
{
33+
$tokens = $this->phpcsFile->getTokens();
34+
$target = $this->getTargetToken($testMarker, [T_NULLABLE, T_INLINE_THEN]);
35+
$tokenArray = $tokens[$target];
36+
37+
$this->assertSame(T_NULLABLE, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_NULLABLE (code)');
38+
$this->assertSame('T_NULLABLE', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_NULLABLE (type)');
39+
40+
}//end testNullable()
41+
42+
43+
/**
44+
* Data provider.
45+
*
46+
* @see testNullable()
47+
*
48+
* @return array<string, array<string>>
49+
*/
50+
public static function dataNullable()
51+
{
52+
return [
53+
'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'],
54+
];
55+
56+
}//end dataNullable()
57+
58+
59+
/**
60+
* Test that "readonly" when not used as the keyword is still tokenized as `T_STRING`.
61+
*
62+
* @param string $testMarker The comment which prefaces the target token in the test file.
63+
*
64+
* @dataProvider dataInlineThen
65+
*
66+
* @return void
67+
*/
68+
public function testInlineThen($testMarker)
69+
{
70+
$tokens = $this->phpcsFile->getTokens();
71+
$target = $this->getTargetToken($testMarker, [T_NULLABLE, T_INLINE_THEN]);
72+
$tokenArray = $tokens[$target];
73+
74+
$this->assertSame(T_INLINE_THEN, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_INLINE_THEN (code)');
75+
$this->assertSame('T_INLINE_THEN', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_INLINE_THEN (type)');
76+
77+
}//end testInlineThen()
78+
79+
80+
/**
81+
* Data provider.
82+
*
83+
* @see testInlineThen()
84+
*
85+
* @return array<string, array<string>>
86+
*/
87+
public static function dataInlineThen()
88+
{
89+
return [
90+
'ternary in property default value' => ['/* testInlineThenInPropertyDefaultValue */'],
91+
];
92+
93+
}//end dataInlineThen()
94+
95+
96+
}//end class

0 commit comments

Comments
 (0)