Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHP 8.1: Added support for readonly keyword #3480

Merged
merged 2 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
<file baseinstalldir="" name="NamedFunctionCallArgumentsTest.php" role="test" />
<file baseinstalldir="" name="NullsafeObjectOperatorTest.inc" role="test" />
<file baseinstalldir="" name="NullsafeObjectOperatorTest.php" role="test" />
<file baseinstalldir="" name="ReadonlyTest.inc" role="test" />
<file baseinstalldir="" name="ReadonlyTest.php" role="test" />
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.inc" role="test" />
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.php" role="test" />
<file baseinstalldir="" name="ShortArrayTest.inc" role="test" />
Expand Down Expand Up @@ -2092,6 +2094,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
<install as="CodeSniffer/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" name="tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.php" name="tests/Core/Tokenizer/ReadonlyTest.php" />
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.inc" name="tests/Core/Tokenizer/ReadonlyTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
Expand Down Expand Up @@ -2182,6 +2186,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
<install as="CodeSniffer/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" name="tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.php" name="tests/Core/Tokenizer/ReadonlyTest.php" />
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.inc" name="tests/Core/Tokenizer/ReadonlyTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
Expand Down
6 changes: 6 additions & 0 deletions src/Files/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -1811,13 +1811,15 @@ public function getMemberProperties($stackPtr)
T_PROTECTED => T_PROTECTED,
T_STATIC => T_STATIC,
T_VAR => T_VAR,
T_READONLY => T_READONLY,
];

$valid += Util\Tokens::$emptyTokens;

$scope = 'public';
$scopeSpecified = false;
$isStatic = false;
$isReadonly = false;

$startOfStatement = $this->findPrevious(
[
Expand Down Expand Up @@ -1850,6 +1852,9 @@ public function getMemberProperties($stackPtr)
case T_STATIC:
$isStatic = true;
break;
case T_READONLY:
$isReadonly = true;
break;
}
}//end for

Expand Down Expand Up @@ -1901,6 +1906,7 @@ public function getMemberProperties($stackPtr)
'scope' => $scope,
'scope_specified' => $scopeSpecified,
'is_static' => $isStatic,
'is_readonly' => $isReadonly,
'type' => $type,
'type_token' => $typeToken,
'type_end_token' => $typeEndToken,
Expand Down
67 changes: 67 additions & 0 deletions src/Tokenizers/PHP.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ class PHP extends Tokenizer
T_PRIVATE => 7,
T_PUBLIC => 6,
T_PROTECTED => 9,
T_READONLY => 8,
T_REQUIRE => 7,
T_REQUIRE_ONCE => 12,
T_RETURN => 6,
Expand Down Expand Up @@ -2812,6 +2813,72 @@ protected function processAdditional()
$this->tokens[$x]['code'] = T_STRING;
$this->tokens[$x]['type'] = 'T_STRING';
}
} else if (($this->tokens[$i]['code'] === T_STRING && strtolower($this->tokens[$i]['content']) === 'readonly')
|| $this->tokens[$i]['code'] === T_READONLY
) {
/*
"readonly" keyword support
PHP < 8.1: Converts T_STRING to T_READONLY
PHP >= 8.1: Converts some T_READONLY to T_STRING because token_get_all() without the TOKEN_PARSE flag cannot distinguish between them in some situations
*/

$allowedAfter = [
T_STRING => T_STRING,
T_NS_SEPARATOR => T_NS_SEPARATOR,
T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED,
T_NAME_RELATIVE => T_NAME_RELATIVE,
T_NAME_QUALIFIED => T_NAME_QUALIFIED,
T_TYPE_UNION => T_TYPE_UNION,
T_BITWISE_OR => T_BITWISE_OR,
T_ARRAY => T_ARRAY,
T_CALLABLE => T_CALLABLE,
T_SELF => T_SELF,
T_PARENT => T_PARENT,
T_NULL => T_FALSE,
T_NULLABLE => T_NULLABLE,
T_STATIC => T_STATIC,
T_PUBLIC => T_PUBLIC,
T_PROTECTED => T_PROTECTED,
T_PRIVATE => T_PRIVATE,
T_VAR => T_VAR,
];

$shouldBeReadonly = true;

for ($x = ($i + 1); $x < $numTokens; $x++) {
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
continue;
}

if ($this->tokens[$x]['code'] === T_VARIABLE) {
break;
}

if (isset($allowedAfter[$this->tokens[$x]['code']]) === false) {
$shouldBeReadonly = false;
break;
}
}

if ($this->tokens[$i]['code'] === T_STRING && $shouldBeReadonly === true) {
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$line = $this->tokens[$i]['line'];
echo "\t* token $i on line $line changed from T_STRING to T_READONLY".PHP_EOL;
}

$this->tokens[$i]['code'] = T_READONLY;
$this->tokens[$i]['type'] = 'T_READONLY';
} else if ($this->tokens[$i]['code'] === T_READONLY && $shouldBeReadonly === false) {
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$line = $this->tokens[$i]['line'];
echo "\t* token $i on line $line changed from T_READONLY to T_STRING".PHP_EOL;
}

$this->tokens[$i]['code'] = T_STRING;
$this->tokens[$i]['type'] = 'T_STRING';
}

continue;
}//end if

if (($this->tokens[$i]['code'] !== T_CASE
Expand Down
4 changes: 4 additions & 0 deletions src/Util/Tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
define('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG', 'PHPCS_T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG');
}

if (defined('T_READONLY') === false) {
define('T_READONLY', 'PHPCS_T_READONLY');
}

// Tokens used for parsing doc blocks.
define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR');
define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE');
Expand Down
5 changes: 5 additions & 0 deletions tests/Core/File/GetMemberPropertiesTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ $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;

/* testPHP81NotReadonly */
private string $notReadonly;
/* testPHP81Readonly */
public readonly int $readonly;
};

$anon = class {
Expand Down
22 changes: 22 additions & 0 deletions tests/Core/File/GetMemberPropertiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,28 @@ public function dataGetMemberProperties()
'nullable_type' => false,
],
],
[
'/* testPHP81NotReadonly */',
[
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
'is_readonly' => false,
'type' => 'string',
'nullable_type' => false,
],
],
[
'/* testPHP81Readonly */',
[
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
'is_readonly' => true,
'type' => 'int',
'nullable_type' => false,
],
],
[
'/* testPHP8PropertySingleAttribute */',
[
Expand Down
92 changes: 92 additions & 0 deletions tests/Core/Tokenizer/ReadonlyTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

class Foo
{
/* testReadonlyProperty */
readonly int $readonlyProperty;
/* testVarReadonlyProperty */
var readonly int $varReadonlyProperty;
/* testReadonlyVarProperty */
readonly var int $testReadonlyVarProperty;
/* testStaticReadonlyProperty */
static readonly int $staticReadonlyProperty;
/* testReadonlyStaticProperty */
readonly static int $readonlyStaticProperty;
/* testReadonlyPropertyWithoutType */
readonly $propertyWithoutType;
/* testPublicReadonlyProperty */
public readonly int $publicReadonlyProperty;
/* testProtectedReadonlyProperty */
protected readonly int $protectedReadonlyProperty;
/* testPrivateReadonlyProperty */
private readonly int $privateReadonlyProperty;
/* testPublicReadonlyPropertyWithReadonlyFirst */
readonly public int $publicReadonlyProperty;
/* testProtectedReadonlyPropertyWithReadonlyFirst */
readonly protected int $protectedReadonlyProperty;
/* testPrivateReadonlyPropertyWithReadonlyFirst */
readonly private int $privateReadonlyProperty;
/* testReadonlyWithCommentsInDeclaration */
private /* Comment */ readonly /* Comment */ int /* Comment */ $readonlyPropertyWithCommentsInDeclaration;
/* testReadonlyWithNullableProperty */
private readonly ?int $nullableProperty;
/* testReadonlyNullablePropertyWithUnionTypeHintAndNullFirst */
private readonly null|int $nullablePropertyWithUnionTypeHintAndNullFirst;
/* testReadonlyNullablePropertyWithUnionTypeHintAndNullLast */
private readonly int|null $nullablePropertyWithUnionTypeHintAndNullLast;
/* testReadonlyPropertyWithArrayTypeHint */
private readonly array $arrayProperty;
/* testReadonlyPropertyWithSelfTypeHint */
private readonly self $selfProperty;
/* testReadonlyPropertyWithParentTypeHint */
private readonly parent $parentProperty;
/* testReadonlyPropertyWithFullyQualifiedTypeHint */
private readonly \stdClass $propertyWithFullyQualifiedTypeHint;

kukulich marked this conversation as resolved.
Show resolved Hide resolved
/* testReadonlyIsCaseInsensitive */
public ReAdOnLy string $caseInsensitiveProperty;

/* testReadonlyConstructorPropertyPromotion */
public function __construct(private readonly bool $constructorPropertyPromotion)
{
}
}

$anonymousClass = new class () {
/* testReadonlyPropertyInAnonymousClass */
public readonly int $property;
};

class ClassName {
/* testReadonlyUsedAsClassConstantName */
const READONLY = 'readonly';

/* testReadonlyUsedAsMethodName */
public function readonly() {
// Do something.

/* testReadonlyUsedAsPropertyName */
$this->readonly = 'foo';

/* testReadonlyPropertyInTernaryOperator */
$isReadonly = $this->readonly ? true : false;
}
}

/* testReadonlyUsedAsFunctionName */
function readonly()
{
}

/* testReadonlyUsedAsNamespaceName */
namespace Readonly;
/* testReadonlyUsedAsPartOfNamespaceName */
namespace My\Readonly\Collection;
/* testReadonlyAsFunctionCall */
$var = readonly($a, $b);
/* testClassConstantFetchWithReadonlyAsConstantName */
echo ClassName::READONLY;

/* testParseErrorLiveCoding */
// This must be the last test in the file.
readonly
Loading