diff --git a/PHPCSUtils/BackCompat/BCFile.php b/PHPCSUtils/BackCompat/BCFile.php index 121f8b24..9c761144 100644 --- a/PHPCSUtils/BackCompat/BCFile.php +++ b/PHPCSUtils/BackCompat/BCFile.php @@ -141,6 +141,8 @@ public static function getDeclarationName(File $phpcsFile, $stackPtr) * ```php * 'property_visibility' => string, // The property visibility as declared. * 'visibility_token' => integer, // The stack pointer to the visibility modifier token. + * 'property_readonly' => bool, // TRUE if the readonly keyword was found. + * 'readonly_token' => integer, // The stack pointer to the readonly modifier token. * ``` * * PHPCS cross-version compatible version of the `File::getMethodParameters()` method. @@ -217,6 +219,7 @@ public static function getMethodParameters(File $phpcsFile, $stackPtr) $typeHintEndToken = false; $nullableType = false; $visibilityToken = null; + $readonlyToken = null; for ($i = $paramStart; $i <= $closer; $i++) { // Check to see if this token has a parenthesis or bracket opener. If it does @@ -345,6 +348,11 @@ public static function getMethodParameters(File $phpcsFile, $stackPtr) $visibilityToken = $i; } break; + case T_READONLY: + if ($defaultStart === null) { + $readonlyToken = $i; + } + break; case T_CLOSE_PARENTHESIS: case T_COMMA: // If it's null, then there must be no parameters for this @@ -377,6 +385,12 @@ public static function getMethodParameters(File $phpcsFile, $stackPtr) if ($visibilityToken !== null) { $vars[$paramCount]['property_visibility'] = $tokens[$visibilityToken]['content']; $vars[$paramCount]['visibility_token'] = $visibilityToken; + $vars[$paramCount]['property_readonly'] = false; + } + + if ($readonlyToken !== null) { + $vars[$paramCount]['property_readonly'] = true; + $vars[$paramCount]['readonly_token'] = $readonlyToken; } if ($tokens[$i]['code'] === T_COMMA) { @@ -400,6 +414,7 @@ public static function getMethodParameters(File $phpcsFile, $stackPtr) $typeHintEndToken = false; $nullableType = false; $visibilityToken = null; + $readonlyToken = null; ++$paramCount; break; diff --git a/PHPCSUtils/Tokens/Collections.php b/PHPCSUtils/Tokens/Collections.php index 1af6e74f..3b221cf4 100644 --- a/PHPCSUtils/Tokens/Collections.php +++ b/PHPCSUtils/Tokens/Collections.php @@ -503,6 +503,7 @@ class Collections * DEPRECATED: Modifier keywords which can be used for a property declaration. * * @since 1.0.0-alpha1 + * @since 1.0.0-alpha4 Added the T_READONLY token for PHP 8.1 readonly properties. * * @deprecated 1.0.0-alpha4 Use the {@see Collections::propertyModifierKeywords()} method instead. * @@ -514,6 +515,7 @@ class Collections \T_PROTECTED => \T_PROTECTED, \T_STATIC => \T_STATIC, \T_VAR => \T_VAR, + \T_READONLY => \T_READONLY, ]; /** diff --git a/PHPCSUtils/Utils/FunctionDeclarations.php b/PHPCSUtils/Utils/FunctionDeclarations.php index f636b712..7ad7d1df 100644 --- a/PHPCSUtils/Utils/FunctionDeclarations.php +++ b/PHPCSUtils/Utils/FunctionDeclarations.php @@ -343,6 +343,8 @@ public static function getProperties(File $phpcsFile, $stackPtr) * ```php * 'property_visibility' => string, // The property visibility as declared. * 'visibility_token' => integer, // The stack pointer to the visibility modifier token. + * 'property_readonly' => bool, // TRUE if the readonly keyword was found. + * 'readonly_token' => integer, // The stack pointer to the readonly modifier token. * ``` * * Main differences with the PHPCS version: @@ -361,6 +363,7 @@ public static function getProperties(File $phpcsFile, $stackPtr) * @since 1.0.0-alpha4 Added support for PHP 8.0 constructor property promotion. * @since 1.0.0-alpha4 Added support for PHP 8.0 identifier name tokenization. * @since 1.0.0-alpha4 Added support for PHP 8.0 parameter attributes. + * @since 1.0.0-alpha4 Added support for PHP 8.1 readonly keyword for constructor property promotion. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position in the stack of the function token @@ -426,6 +429,7 @@ public static function getParameters(File $phpcsFile, $stackPtr) $typeHintEndToken = false; $nullableType = false; $visibilityToken = null; + $readonlyToken = null; for ($i = $paramStart; $i <= $closer; $i++) { if (isset(Collections::parameterTypeTokens()[$tokens[$i]['code']]) === true @@ -475,6 +479,10 @@ public static function getParameters(File $phpcsFile, $stackPtr) $visibilityToken = $i; break; + case \T_READONLY: + $readonlyToken = $i; + break; + case \T_CLOSE_PARENTHESIS: case \T_COMMA: // If it's null, then there must be no parameters for this @@ -511,6 +519,12 @@ public static function getParameters(File $phpcsFile, $stackPtr) if ($visibilityToken !== null) { $vars[$paramCount]['property_visibility'] = $tokens[$visibilityToken]['content']; $vars[$paramCount]['visibility_token'] = $visibilityToken; + $vars[$paramCount]['property_readonly'] = false; + } + + if ($readonlyToken !== null) { + $vars[$paramCount]['property_readonly'] = true; + $vars[$paramCount]['readonly_token'] = $readonlyToken; } if ($tokens[$i]['code'] === \T_COMMA) { @@ -534,6 +548,7 @@ public static function getParameters(File $phpcsFile, $stackPtr) $typeHintEndToken = false; $nullableType = false; $visibilityToken = null; + $readonlyToken = null; ++$paramCount; break; diff --git a/PHPCSUtils/Utils/Variables.php b/PHPCSUtils/Utils/Variables.php index 027e5e73..14c92e7c 100644 --- a/PHPCSUtils/Utils/Variables.php +++ b/PHPCSUtils/Utils/Variables.php @@ -89,6 +89,7 @@ class Variables * @since 1.0.0 * @since 1.0.0-alpha4 Added support for PHP 8.0 union types. * @since 1.0.0-alpha4 No longer gets confused by PHP 8.0 property attributes. + * @since 1.0.0-alpha4 Added support for PHP 8.1 readonly properties. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position in the stack of the `T_VARIABLE` token @@ -101,6 +102,7 @@ class Variables * 'scope' => string, // Public, private, or protected. * 'scope_specified' => boolean, // TRUE if the scope was explicitly specified. * 'is_static' => boolean, // TRUE if the static keyword was found. + * 'is_readonly' => boolean, // TRUE if the readonly keyword was found. * 'type' => string, // The type of the var (empty if no type specified). * 'type_token' => integer, // The stack pointer to the start of the type * // or FALSE if there is no type. @@ -133,6 +135,7 @@ public static function getMemberProperties(File $phpcsFile, $stackPtr) $scope = 'public'; $scopeSpecified = false; $isStatic = false; + $isReadonly = false; $startOfStatement = $phpcsFile->findPrevious( [ @@ -165,6 +168,9 @@ public static function getMemberProperties(File $phpcsFile, $stackPtr) case \T_STATIC: $isStatic = true; break; + case \T_READONLY: + $isReadonly = true; + break; } } @@ -205,6 +211,7 @@ public static function getMemberProperties(File $phpcsFile, $stackPtr) 'scope' => $scope, 'scope_specified' => $scopeSpecified, 'is_static' => $isStatic, + 'is_readonly' => $isReadonly, 'type' => $type, 'type_token' => $typeToken, 'type_end_token' => $typeEndToken, diff --git a/Tests/BackCompat/BCFile/GetMemberPropertiesTest.inc b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.inc index 8673049a..034ff308 100644 --- a/Tests/BackCompat/BCFile/GetMemberPropertiesTest.inc +++ b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.inc @@ -242,6 +242,31 @@ $anon = class() { /* testPHP8DuplicateTypeInUnionWhitespaceAndComment */ // Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method. public int |string| /*comment*/ INT $duplicateTypeInUnion; + + /* testPHP81Readonly */ + public readonly int $readonly; + + /* testPHP81ReadonlyWithNullableType */ + public readonly ?array $readonlyWithNullableType; + + /* testPHP81ReadonlyWithUnionType */ + public readonly string|int $readonlyWithUnionType; + + /* testPHP81ReadonlyWithUnionTypeWithNull */ + protected ReadOnly string|null $readonlyWithUnionTypeWithNull; + + /* testPHP81OnlyReadonlyWithUnionType */ + readonly string|int $onlyReadonly; + + /* testPHP81OnlyReadonlyWithUnionTypeMultiple */ + readonly \InterfaceA|\Sub\InterfaceB|false + $onlyReadonly; + + /* testPHP81ReadonlyAndStatic */ + readonly private static ?string $readonlyAndStatic; + + /* testPHP81ReadonlyMixedCase */ + public ReadONLY static $readonlyMixedCase; }; $anon = class { diff --git a/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php index 766a83aa..574248c1 100644 --- a/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php +++ b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php @@ -70,9 +70,6 @@ public function testGetMemberProperties($identifier, $expected) $expected['type_end_token'] += $variable; } - // Temporarily ignore the `is_readonly` key until support for readonly properties has been synced. - unset($result['is_readonly']); - $this->assertSame($expected, $result); } @@ -94,6 +91,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -106,6 +104,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => '?int', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -118,6 +117,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -130,6 +130,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'string', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -142,6 +143,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -154,6 +156,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'bool', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -166,6 +169,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -178,6 +182,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'array', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -190,6 +195,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -202,6 +208,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -214,6 +221,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -226,6 +234,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -238,6 +247,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -250,6 +260,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -262,6 +273,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -274,6 +286,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -286,6 +299,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -298,6 +312,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -310,6 +325,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -322,6 +338,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'float', 'type_token' => -6, // Offset from the T_VARIABLE token. 'type_end_token' => -6, // Offset from the T_VARIABLE token. @@ -334,6 +351,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'float', 'type_token' => -13, // Offset from the T_VARIABLE token. 'type_end_token' => -13, // Offset from the T_VARIABLE token. @@ -346,6 +364,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'type_token' => -6, // Offset from the T_VARIABLE token. 'type_end_token' => -6, // Offset from the T_VARIABLE token. @@ -358,6 +377,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'type_token' => -17, // Offset from the T_VARIABLE token. 'type_end_token' => -17, // Offset from the T_VARIABLE token. @@ -370,6 +390,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -382,6 +403,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -394,6 +416,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -406,6 +429,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -418,6 +442,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -430,6 +455,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -442,6 +468,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -454,6 +481,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -466,6 +494,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -478,6 +507,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -490,6 +520,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?array', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -502,6 +533,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '\MyNamespace\MyClass', 'type_token' => ($php8Names === true) ? -2 : -5, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -514,6 +546,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?ClassName', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -526,6 +559,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?Folder\ClassName', 'type_token' => ($php8Names === true) ? -2 : -4, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -538,6 +572,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '\MyNamespace\MyClass\Foo', 'type_token' => ($php8Names === true) ? -15 : -18, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -550,6 +585,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -566,6 +602,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -578,6 +615,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'type_token' => false, 'type_end_token' => false, @@ -590,6 +628,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => 'miXed', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -602,6 +641,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?mixed', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -614,6 +654,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?namespace\Name', 'type_token' => ($php8Names === true) ? -2 : -4, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -626,6 +667,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'int|float', 'type_token' => -4, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -638,6 +680,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'MyClassA|\Package\MyClassB', 'type_token' => ($php8Names === true) ? -4 : -7, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -650,6 +693,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'array|bool|int|float|NULL|object|string', 'type_token' => -14, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -662,6 +706,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => 'false|mixed|self|parent|iterable|Resource', 'type_token' => -12, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -674,7 +719,9 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, - 'type' => 'callable||void', // Missing static, but that's OK as not an allowed syntax. + 'is_readonly' => false, + // Missing static, but that's OK as not an allowed syntax. + 'type' => 'callable||void', 'type_token' => -6, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. 'nullable_type' => false, @@ -686,6 +733,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?int|float', 'type_token' => -4, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -698,6 +746,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'null', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -710,6 +759,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'false', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -722,6 +772,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'bool|FALSE', 'type_token' => -4, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -734,6 +785,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'object|ClassName', 'type_token' => -4, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -746,6 +798,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'iterable|array|Traversable', 'type_token' => -6, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -758,18 +811,124 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'int|string|INT', 'type_token' => -10, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. 'nullable_type' => false, ], ], + 'php8.1-readonly-property' => [ + '/* testPHP81Readonly */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'int', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + 'php8.1-readonly-property-with-nullable-type' => [ + '/* testPHP81ReadonlyWithNullableType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => '?array', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + 'php8.1-readonly-property-with-union-type' => [ + '/* testPHP81ReadonlyWithUnionType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|int', + 'type_token' => -4, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + 'php8.1-readonly-property-with-union-type-with-null' => [ + '/* testPHP81ReadonlyWithUnionTypeWithNull */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|null', + 'type_token' => -4, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + 'php8.1-readonly-property-with-union-type-no-visibility' => [ + '/* testPHP81OnlyReadonlyWithUnionType */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|int', + 'type_token' => -4, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + 'php8.1-readonly-property-with-multi-union-type-no-visibility' => [ + '/* testPHP81OnlyReadonlyWithUnionTypeMultiple */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'is_readonly' => true, + 'type' => '\InterfaceA|\Sub\InterfaceB|false', + 'type_token' => ($php8Names === true) ? -7 : -11, // Offset from the T_VARIABLE token. + 'type_end_token' => -3, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + 'php8.1-readonly-and-static-property' => [ + '/* testPHP81ReadonlyAndStatic */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => true, + 'is_readonly' => true, + 'type' => '?string', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + 'php8.1-readonly-mixed-case-keyword' => [ + '/* testPHP81ReadonlyMixedCase */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => true, + 'is_readonly' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], 'php8-property-with-single-attribute' => [ '/* testPHP8PropertySingleAttribute */', [ 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'string', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -782,6 +941,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?int|float', 'type_token' => -4, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. @@ -794,6 +954,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'mixed', 'type_token' => -2, // Offset from the T_VARIABLE token. 'type_end_token' => -2, // Offset from the T_VARIABLE token. diff --git a/Tests/BackCompat/BCFile/GetMethodParametersTest.inc b/Tests/BackCompat/BCFile/GetMethodParametersTest.inc index 91e57710..e5918b40 100644 --- a/Tests/BackCompat/BCFile/GetMethodParametersTest.inc +++ b/Tests/BackCompat/BCFile/GetMethodParametersTest.inc @@ -201,6 +201,11 @@ class ConstructorPropertyPromotionAndNormalParams { public function __construct(public int $promotedProp, ?int $normalArg) {} } +class ConstructorPropertyPromotionWithReadOnly { + /* testPHP81ConstructorPropertyPromotionWithReadOnly */ + public function __construct(public readonly ?int $promotedProp, ReadOnly private string|bool &$promotedToo) {} +} + /* testPHP8ConstructorPropertyPromotionGlobalFunction */ // Intentional fatal error. Property promotion not allowed in non-constructor, but that's not the concern of this method. function globalFunction(private $x) {} diff --git a/Tests/BackCompat/BCFile/GetMethodParametersTest.php b/Tests/BackCompat/BCFile/GetMethodParametersTest.php index 217a2ea4..021e2a88 100644 --- a/Tests/BackCompat/BCFile/GetMethodParametersTest.php +++ b/Tests/BackCompat/BCFile/GetMethodParametersTest.php @@ -1748,6 +1748,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes() 'nullable_type' => false, 'property_visibility' => 'public', 'visibility_token' => 6, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => 13, ]; $expected[1] = [ @@ -1768,6 +1769,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes() 'nullable_type' => false, 'property_visibility' => 'protected', 'visibility_token' => 16, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => 23, ]; $expected[2] = [ @@ -1788,6 +1790,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes() 'nullable_type' => false, 'property_visibility' => 'private', 'visibility_token' => 26, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => 33, ]; @@ -1817,6 +1820,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes() 'nullable_type' => false, 'property_visibility' => 'protected', 'visibility_token' => 4, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => 11, ]; $expected[1] = [ @@ -1837,6 +1841,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes() 'nullable_type' => true, 'property_visibility' => 'public', 'visibility_token' => 13, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => 24, ]; $expected[2] = [ @@ -1854,6 +1859,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes() 'nullable_type' => false, 'property_visibility' => 'private', 'visibility_token' => 26, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => false, ]; @@ -1883,6 +1889,7 @@ public function testPHP8ConstructorPropertyPromotionAndNormalParam() 'nullable_type' => false, 'property_visibility' => 'public', 'visibility_token' => 4, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => 9, ]; $expected[1] = [ @@ -1904,6 +1911,56 @@ public function testPHP8ConstructorPropertyPromotionAndNormalParam() $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); } + /** + * Verify recognition of PHP8 constructor with property promotion using PHP 8.1 readonly keyword. + * + * @return void + */ + public function testPHP81ConstructorPropertyPromotionWithReadOnly() + { + $expected = []; + $expected[0] = [ + 'token' => 11, // Offset from the T_FUNCTION token. + 'name' => '$promotedProp', + 'content' => 'public readonly ?int $promotedProp', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '?int', + 'type_hint_token' => 9, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 9, // Offset from the T_FUNCTION token. + 'nullable_type' => true, + 'property_visibility' => 'public', + 'visibility_token' => 4, // Offset from the T_FUNCTION token. + 'property_readonly' => true, + 'readonly_token' => 6, // Offset from the T_FUNCTION token. + 'comma_token' => 12, + ]; + $expected[1] = [ + 'token' => 23, // Offset from the T_FUNCTION token. + 'name' => '$promotedToo', + 'content' => 'ReadOnly private string|bool &$promotedToo', + 'has_attributes' => false, + 'pass_by_reference' => true, + 'reference_token' => 22, // Offset from the T_FUNCTION token. + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'string|bool', + 'type_hint_token' => 18, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 20, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'property_visibility' => 'private', + 'visibility_token' => 16, // Offset from the T_FUNCTION token. + 'property_readonly' => true, + 'readonly_token' => 14, // Offset from the T_FUNCTION token. + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + /** * Verify behaviour when a non-constructor function uses PHP 8 property promotion syntax. * @@ -1927,6 +1984,7 @@ public function testPHP8ConstructorPropertyPromotionGlobalFunction() 'nullable_type' => false, 'property_visibility' => 'private', 'visibility_token' => 4, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => false, ]; @@ -1956,6 +2014,7 @@ public function testPHP8ConstructorPropertyPromotionAbstractMethod() 'nullable_type' => false, 'property_visibility' => 'public', 'visibility_token' => 4, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => 9, ]; $expected[1] = [ @@ -1973,6 +2032,7 @@ public function testPHP8ConstructorPropertyPromotionAbstractMethod() 'nullable_type' => false, 'property_visibility' => 'private', 'visibility_token' => 11, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => false, ]; @@ -2036,6 +2096,7 @@ public function testParameterAttributesInFunctionDeclaration() 'nullable_type' => false, 'property_visibility' => 'private', 'visibility_token' => ($php8Names === true) ? 10 : 13, // Offset from the T_FUNCTION token. + 'property_readonly' => false, 'comma_token' => ($php8Names === true) ? 15 : 18, // Offset from the T_FUNCTION token. ]; $expected[1] = [ @@ -2390,6 +2451,9 @@ protected function getMethodParametersTestHelper($marker, $expected, $targetType if (isset($param['visibility_token'])) { $expected[$key]['visibility_token'] += $target; } + if (isset($param['readonly_token'])) { + $expected[$key]['readonly_token'] += $target; + } } $this->assertSame($expected, $found); diff --git a/Tests/Utils/FunctionDeclarations/GetParametersTest.php b/Tests/Utils/FunctionDeclarations/GetParametersTest.php index b6c98683..92c6b353 100644 --- a/Tests/Utils/FunctionDeclarations/GetParametersTest.php +++ b/Tests/Utils/FunctionDeclarations/GetParametersTest.php @@ -143,6 +143,9 @@ protected function getMethodParametersTestHelper($marker, $expected, $targetType if (isset($param['visibility_token'])) { $expected[$key]['visibility_token'] += $target; } + if (isset($param['readonly_token'])) { + $expected[$key]['readonly_token'] += $target; + } } $this->assertSame($expected, $found);