Skip to content

Commit

Permalink
Merge pull request #309 from PHPCSStandards/php-8.3/tokenizer-readonl…
Browse files Browse the repository at this point in the history
…y-anonymous-classes

PHP 8.3 | Tokenizer/PHP: add support for readonly anonymous classes
  • Loading branch information
jrfnl authored Feb 2, 2024
2 parents 90ea70c + 9244858 commit dc22022
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 3 deletions.
20 changes: 19 additions & 1 deletion src/Tokenizers/PHP.php
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,23 @@ protected function tokenize($string)
$preserveKeyword = true;
}

// `new readonly class` should be preserved.
if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW
&& strtolower($token[1]) === 'readonly'
) {
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
if (is_array($tokens[$i]) === false
|| isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
) {
break;
}
}

if (is_array($tokens[$i]) === true && $tokens[$i][0] === T_CLASS) {
$preserveKeyword = true;
}
}

// `new class extends` `new class implements` should be preserved
if (($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS)
&& $finalTokens[$lastNotEmptyToken]['code'] === T_CLASS
Expand Down Expand Up @@ -1315,7 +1332,8 @@ protected function tokenize($string)

if ($tokenIsArray === true
&& strtolower($token[1]) === 'readonly'
&& isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
&& (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
|| $finalTokens[$lastNotEmptyToken]['code'] === T_NEW)
) {
// Get the next non-whitespace token.
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
Expand Down
10 changes: 10 additions & 0 deletions tests/Core/Tokenizer/AnonClassParenthesisOwnerTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ $anonClass = new class {
function __construct() {}
};

/* testReadonlyNoParentheses */
$anonClass = new readonly class {
function __construct() {}
};

/* testNoParenthesesAndEmptyTokens */
$anonClass = new class // phpcs:ignore Standard.Cat
{
Expand All @@ -14,6 +19,11 @@ $anonClass = new class // phpcs:ignore Standard.Cat
/* testWithParentheses */
$anonClass = new class() {};

/* testReadonlyWithParentheses */
$anonClass = new readonly class() {
function __construct() {}
};

/* testWithParenthesesAndEmptyTokens */
$anonClass = new class /*comment */
() {};
6 changes: 6 additions & 0 deletions tests/Core/Tokenizer/AnonClassParenthesisOwnerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public static function dataAnonClassNoParentheses()
'plain' => [
'testMarker' => '/* testNoParentheses */',
],
'readonly' => [
'testMarker' => '/* testReadonlyNoParentheses */',
],
'declaration contains comments and extra whitespace' => [
'testMarker' => '/* testNoParenthesesAndEmptyTokens */',
],
Expand Down Expand Up @@ -139,6 +142,9 @@ public static function dataAnonClassWithParentheses()
'plain' => [
'testMarker' => '/* testWithParentheses */',
],
'readonly' => [
'testMarker' => '/* testReadonlyWithParentheses */',
],
'declaration contains comments and extra whitespace' => [
'testMarker' => '/* testWithParenthesesAndEmptyTokens */',
],
Expand Down
13 changes: 13 additions & 0 deletions tests/Core/Tokenizer/BackfillReadonlyTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ class ReadonlyWithDisjunctiveNormalForm
public function readonly (A&B $param): void {}
}

/* testReadonlyAnonClassWithParens */
$anon = new readonly class() {};

/* testReadonlyAnonClassWithoutParens */
$anon = new Readonly class {};

/* testReadonlyAnonClassWithCommentsAndWhitespace */
$anon = new
// comment
READONLY
// phpcs:ignore Stnd.Cat.Sniff
class {};

/* testParseErrorLiveCoding */
// This must be the last test in the file.
readonly
11 changes: 11 additions & 0 deletions tests/Core/Tokenizer/BackfillReadonlyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ public static function dataReadonly()
'property declaration, constructor property promotion, DNF type and reference' => [
'testMarker' => '/* testReadonlyConstructorPropertyPromotionWithDNFAndReference */',
],
'anon class declaration, with parentheses' => [
'testMarker' => '/* testReadonlyAnonClassWithParens */',
],
'anon class declaration, without parentheses' => [
'testMarker' => '/* testReadonlyAnonClassWithoutParens */',
'testContent' => 'Readonly',
],
'anon class declaration, with comments and whitespace' => [
'testMarker' => '/* testReadonlyAnonClassWithCommentsAndWhitespace */',
'testContent' => 'READONLY',
],
'live coding / parse error' => [
'testMarker' => '/* testParseErrorLiveCoding */',
],
Expand Down
6 changes: 5 additions & 1 deletion tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,16 @@ namespace /* testNamespaceNameIsString1 */ my\ /* testNamespaceNameIsString2 */
/* testVarIsKeyword */ var $var;
/* testStaticIsKeyword */ static $static;

/* testReadonlyIsKeyword */ readonly $readonly;
/* testReadonlyIsKeywordForProperty */ readonly $readonly;

/* testFinalIsKeyword */ final /* testFunctionIsKeyword */ function someFunction(
/* testCallableIsKeyword */
callable $callable,
) {
$anon = new /* testReadonlyIsKeywordForAnonClass */ readonly class() {
public function foo() {}
};

/* testReturnIsKeyword */
return $this;
}
Expand Down
6 changes: 5 additions & 1 deletion tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public static function dataKeywords()
'expectedTokenType' => 'T_STATIC',
],
'readonly: property declaration' => [
'testMarker' => '/* testReadonlyIsKeyword */',
'testMarker' => '/* testReadonlyIsKeywordForProperty */',
'expectedTokenType' => 'T_READONLY',
],
'final: function declaration' => [
Expand All @@ -247,6 +247,10 @@ public static function dataKeywords()
'testMarker' => '/* testCallableIsKeyword */',
'expectedTokenType' => 'T_CALLABLE',
],
'readonly: anon class declaration' => [
'testMarker' => '/* testReadonlyIsKeywordForAnonClass */',
'expectedTokenType' => 'T_READONLY',
],
'return: statement' => [
'testMarker' => '/* testReturnIsKeyword */',
'expectedTokenType' => 'T_RETURN',
Expand Down

0 comments on commit dc22022

Please sign in to comment.