Skip to content

Commit 96bc9dd

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 2fbf9e3 commit 96bc9dd

File tree

7 files changed

+67
-3
lines changed

7 files changed

+67
-3
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
@@ -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

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
@@ -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

Lines changed: 5 additions & 1 deletion
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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ public static function dataKeywords()
232232
'expectedTokenType' => 'T_STATIC',
233233
],
234234
'readonly: property declaration' => [
235-
'testMarker' => '/* testReadonlyIsKeyword */',
235+
'testMarker' => '/* testReadonlyIsKeywordForProperty */',
236236
'expectedTokenType' => 'T_READONLY',
237237
],
238238
'final: function declaration' => [
@@ -247,6 +247,10 @@ public static function dataKeywords()
247247
'testMarker' => '/* testCallableIsKeyword */',
248248
'expectedTokenType' => 'T_CALLABLE',
249249
],
250+
],
251+
'readonly: anon class declaration' => [
252+
'testMarker' => '/* testReadonlyIsKeywordForAnonClass */',
253+
'expectedTokenType' => 'T_READONLY',
250254
'return: statement' => [
251255
'testMarker' => '/* testReturnIsKeyword */',
252256
'expectedTokenType' => 'T_RETURN',

0 commit comments

Comments
 (0)