Skip to content

Commit 24e3ba6

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 d55602b commit 24e3ba6

7 files changed

+69
-3
lines changed

src/Tokenizers/PHP.php

+19-1
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,23 @@ protected function tokenize($string)
630630
$preserveKeyword = true;
631631
}
632632

633+
// `new readonly class` should be preserved.
634+
if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW
635+
&& strtolower($token[1]) === 'readonly'
636+
) {
637+
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
638+
if (is_array($tokens[$i]) === false
639+
|| isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
640+
) {
641+
break;
642+
}
643+
}
644+
645+
if (is_array($tokens[$i]) === true && $tokens[$i][0] === T_CLASS) {
646+
$preserveKeyword = true;
647+
}
648+
}
649+
633650
// `new class extends` `new class implements` should be preserved
634651
if (($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS)
635652
&& $finalTokens[$lastNotEmptyToken]['code'] === T_CLASS
@@ -1249,7 +1266,8 @@ protected function tokenize($string)
12491266

12501267
if ($tokenIsArray === true
12511268
&& strtolower($token[1]) === 'readonly'
1252-
&& isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
1269+
&& (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
1270+
|| $finalTokens[$lastNotEmptyToken]['code'] === T_NEW)
12531271
) {
12541272
// Get the next non-whitespace token.
12551273
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {

tests/Core/Tokenizer/AnonClassParenthesisOwnerTest.inc

+10
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

+6
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ public static function dataAnonClassNoParentheses()
7676
'plain' => [
7777
'testMarker' => '/* testNoParentheses */',
7878
],
79+
'readonly' => [
80+
'testMarker' => '/* testReadonlyNoParentheses */',
81+
],
7982
'declaration contains comments and extra whitespace' => [
8083
'testMarker' => '/* testNoParenthesesAndEmptyTokens */',
8184
],
@@ -139,6 +142,9 @@ public static function dataAnonClassWithParentheses()
139142
'plain' => [
140143
'testMarker' => '/* testWithParentheses */',
141144
],
145+
'readonly' => [
146+
'testMarker' => '/* testReadonlyWithParentheses */',
147+
],
142148
'declaration contains comments and extra whitespace' => [
143149
'testMarker' => '/* testWithParenthesesAndEmptyTokens */',
144150
],

tests/Core/Tokenizer/BackfillReadonlyTest.inc

+13
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ class ReadonlyWithDisjunctiveNormalForm
136136
public function readonly (A&B $param): void {}
137137
}
138138

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

tests/Core/Tokenizer/BackfillReadonlyTest.php

+11
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ public static function dataReadonly()
151151
'property declaration, constructor property promotion, DNF type and reference' => [
152152
'testMarker' => '/* testReadonlyConstructorPropertyPromotionWithDNFAndReference */',
153153
],
154+
'anon class declaration, with parentheses' => [
155+
'testMarker' => '/* testReadonlyAnonClassWithParens */',
156+
],
157+
'anon class declaration, without parentheses' => [
158+
'testMarker' => '/* testReadonlyAnonClassWithoutParens */',
159+
'testContent' => 'Readonly',
160+
],
161+
'anon class declaration, with comments and whitespace' => [
162+
'testMarker' => '/* testReadonlyAnonClassWithCommentsAndWhitespace */',
163+
'testContent' => 'READONLY',
164+
],
154165
'live coding / parse error' => [
155166
'testMarker' => '/* testParseErrorLiveCoding */',
156167
],

tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc

+5-1
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,16 @@ namespace /* testNamespaceNameIsString1 */ my\ /* testNamespaceNameIsString2 */
9999
/* testVarIsKeyword */ var $var;
100100
/* testStaticIsKeyword */ static $static;
101101

102-
/* testReadonlyIsKeyword */ readonly $readonly;
102+
/* testReadonlyIsKeywordForProperty */ readonly $readonly;
103103

104104
/* testFinalIsKeyword */ final /* testFunctionIsKeyword */ function someFunction(
105105
/* testCallableIsKeyword */
106106
callable $callable,
107107
) {
108+
$anon = new /* testReadonlyIsKeywordForAnonClass */ readonly class() {
109+
public function foo() {}
110+
};
111+
108112
/* testReturnIsKeyword */
109113
return $this;
110114
}

tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ public static function dataKeywords()
235235
'expectedTokenType' => 'T_STATIC',
236236
],
237237
'readonly: property declaration' => [
238-
'testMarker' => '/* testReadonlyIsKeyword */',
238+
'testMarker' => '/* testReadonlyIsKeywordForProperty */',
239239
'expectedTokenType' => 'T_READONLY',
240240
],
241241
'final: function declaration' => [
@@ -250,6 +250,10 @@ public static function dataKeywords()
250250
'testMarker' => '/* testCallableIsKeyword */',
251251
'expectedTokenType' => 'T_CALLABLE',
252252
],
253+
'readonly: anon class declaration' => [
254+
'testMarker' => '/* testReadonlyIsKeywordForAnonClass */',
255+
'expectedTokenType' => 'T_READONLY',
256+
],
253257
'return: statement' => [
254258
'testMarker' => '/* testReturnIsKeyword */',
255259
'expectedTokenType' => 'T_RETURN',

0 commit comments

Comments
 (0)