Skip to content

Commit 9f2c3c2

Browse files
committed
PHP 8.3 | Tokenizer/PHP: add support for readonly anonymous classes
PHP 8.3 introduced readonly anonymous classes, fixing an oversight in the PHP 8.2 introduction of readonly classes. As things were, for PHP 8.1+, the tokenizer would change the token code for the `readonly` keyword from `T_READONLY` to `T_STRING` in the "context sensitive keyword" layer, thinking it to be a class name. And for PHP < 8.1, the readonly polyfill would ignore the token as it being preceded by the `new` keyword would be seen as conflicting with the "context sensitive keyword" layer, which meant it would not be re-tokenized from `T_STRING` to `T_READONLY`. This commit fixes both. Includes adding tests in a number of pre-existing test classes to cover this change.
1 parent 50a5645 commit 9f2c3c2

File tree

7 files changed

+71
-4
lines changed

7 files changed

+71
-4
lines changed

src/Tokenizers/PHP.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,21 @@ protected function tokenize($string)
621621
$preserveKeyword = true;
622622
}
623623

624+
// `new readonly class` should be preserved.
625+
if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW) {
626+
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
627+
if (is_array($tokens[$i]) === false
628+
|| isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
629+
) {
630+
break;
631+
}
632+
}
633+
634+
if (is_array($tokens[$i]) === true && $tokens[$i][0] === T_CLASS) {
635+
$preserveKeyword = true;
636+
}
637+
}
638+
624639
// `new class extends` `new class implements` should be preserved
625640
if (($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS)
626641
&& $finalTokens[$lastNotEmptyToken]['code'] === T_CLASS
@@ -1315,7 +1330,8 @@ protected function tokenize($string)
13151330

13161331
if ($tokenIsArray === true
13171332
&& strtolower($token[1]) === 'readonly'
1318-
&& isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
1333+
&& (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
1334+
|| $finalTokens[$lastNotEmptyToken]['code'] === T_NEW)
13191335
) {
13201336
// Get the next non-whitespace token.
13211337
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {

tests/Core/Tokenizer/AnonClassParenthesisOwnerTest.inc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ $anonClass = new class {
55
function __construct() {}
66
};
77

8+
/* testReadonlyNoParentheses */
9+
$anonClass = new readonly class {
10+
function __construct() {}
11+
};
12+
813
/* testNoParenthesesAndEmptyTokens */
914
$anonClass = new class // phpcs:ignore Standard.Cat
1015
{
@@ -14,6 +19,11 @@ $anonClass = new class // phpcs:ignore Standard.Cat
1419
/* testWithParentheses */
1520
$anonClass = new class() {};
1621

22+
/* testReadonlyWithParentheses */
23+
$anonClass = new readonly class() {
24+
function __construct() {}
25+
};
26+
1727
/* testWithParenthesesAndEmptyTokens */
1828
$anonClass = new class /*comment */
1929
() {};

tests/Core/Tokenizer/AnonClassParenthesisOwnerTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ public static function dataAnonClassNoParentheses()
7878
'plain' => [
7979
'testMarker' => '/* testNoParentheses */',
8080
],
81+
'readonly' => [
82+
'testMarker' => '/* testReadonlyNoParentheses */',
83+
],
8184
'declaration contains comments and extra whitespace' => [
8285
'testMarker' => '/* testNoParenthesesAndEmptyTokens */',
8386
],
@@ -141,6 +144,9 @@ public static function dataAnonClassWithParentheses()
141144
'plain' => [
142145
'testMarker' => '/* testWithParentheses */',
143146
],
147+
'readonly' => [
148+
'testMarker' => '/* testReadonlyWithParentheses */',
149+
],
144150
'declaration contains comments and extra whitespace' => [
145151
'testMarker' => '/* testWithParenthesesAndEmptyTokens */',
146152
],

tests/Core/Tokenizer/BackfillReadonlyTest.inc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ class ReadonlyWithDisjunctiveNormalForm
138138
public function readonly (A&B $param): void {}
139139
}
140140

141+
/* testReadonlyAnonClassWithParens */
142+
$anon = new readonly class() {};
143+
144+
/* testReadonlyAnonClassWithoutParens */
145+
$anon = new Readonly class {};
146+
147+
/* testReadonlyAnonClassWithCommentsAndWhitespace */
148+
$anon = new
149+
// comment
150+
ReadOnly
151+
// phpcs:ignore Stnd.Cat.Sniff
152+
class {};
153+
141154
/* testParseErrorLiveCoding */
142155
// This must be the last test in the file.
143156
readonly

tests/Core/Tokenizer/BackfillReadonlyTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,17 @@ public static function dataReadonly()
153153
'property declaration, constructor property promotion, DNF type and reference' => [
154154
'testMarker' => '/* testReadonlyConstructorPropertyPromotionWithDNFAndReference */',
155155
],
156+
'anon class declaration, with parentheses' => [
157+
'testMarker' => '/* testReadonlyAnonClassWithParens */',
158+
],
159+
'anon class declaration, without parentheses' => [
160+
'testMarker' => '/* testReadonlyAnonClassWithoutParens */',
161+
'testContent' => 'Readonly',
162+
],
163+
'anon class declaration, with comments and whitespace' => [
164+
'testMarker' => '/* testReadonlyAnonClassWithCommentsAndWhitespace */',
165+
'testContent' => 'ReadOnly',
166+
],
156167
'live coding / parse error' => [
157168
'testMarker' => '/* testParseErrorLiveCoding */',
158169
],

tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ namespace /* testNamespaceNameIsString1 */ my\ /* testNamespaceNameIsString2 */
105105
/* testVarIsKeyword */ var $var;
106106
/* testStaticIsKeyword */ static $static;
107107

108-
/* testReadonlyIsKeyword */ readonly $readonly;
108+
/* testReadonlyIsKeywordForProperty */ readonly $readonly;
109109

110110
/* testFinalIsKeyword */ final /* testFunctionIsKeyword */ function someFunction(
111111
/* testCallableIsKeyword */
@@ -115,6 +115,10 @@ namespace /* testNamespaceNameIsString1 */ my\ /* testNamespaceNameIsString2 */
115115
/* testParentIsKeyword */
116116
parent $parent
117117
) {
118+
$anon = new /* testReadonlyIsKeywordForAnonClass */ readonly class() {
119+
public function foo() {}
120+
};
121+
118122
/* testReturnIsKeyword */
119123
return $this;
120124
}

tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ final class ContextSensitiveKeywordsTest extends AbstractMethodUnitTest
2929
public function testStrings($testMarker)
3030
{
3131
$tokens = self::$phpcsFile->getTokens();
32-
$target = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_STRING, T_NULL, T_FALSE, T_TRUE, T_PARENT, T_SELF]));
32+
$target = $this->getTargetToken(
33+
$testMarker,
34+
(Tokens::$contextSensitiveKeywords + [T_ANON_CLASS, T_MATCH_DEFAULT, T_PARENT, T_SELF, T_STRING, T_NULL, T_FALSE, T_TRUE])
35+
);
3336
$tokenArray = $tokens[$target];
3437

3538
$this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)');
@@ -255,7 +258,7 @@ public static function dataKeywords()
255258
'expectedTokenType' => 'T_STATIC',
256259
],
257260
'readonly: property declaration' => [
258-
'testMarker' => '/* testReadonlyIsKeyword */',
261+
'testMarker' => '/* testReadonlyIsKeywordForProperty */',
259262
'expectedTokenType' => 'T_READONLY',
260263
],
261264
'final: function declaration' => [
@@ -278,6 +281,10 @@ public static function dataKeywords()
278281
'testMarker' => '/* testParentIsKeyword */',
279282
'expectedTokenType' => 'T_PARENT',
280283
],
284+
'readonly: anon class declaration' => [
285+
'testMarker' => '/* testReadonlyIsKeywordForAnonClass */',
286+
'expectedTokenType' => 'T_READONLY',
287+
],
281288
'return: statement' => [
282289
'testMarker' => '/* testReturnIsKeyword */',
283290
'expectedTokenType' => 'T_RETURN',

0 commit comments

Comments
 (0)