Skip to content

Commit 0033bb0

Browse files
committed
PHP 8.1: Added support for "readonly" keyword
1 parent 5fb9b64 commit 0033bb0

File tree

5 files changed

+393
-0
lines changed

5 files changed

+393
-0
lines changed

package.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
141141
<file baseinstalldir="" name="NamedFunctionCallArgumentsTest.php" role="test" />
142142
<file baseinstalldir="" name="NullsafeObjectOperatorTest.inc" role="test" />
143143
<file baseinstalldir="" name="NullsafeObjectOperatorTest.php" role="test" />
144+
<file baseinstalldir="" name="ReadonlyTest.inc" role="test" />
145+
<file baseinstalldir="" name="ReadonlyTest.php" role="test" />
144146
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.inc" role="test" />
145147
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.php" role="test" />
146148
<file baseinstalldir="" name="ShortArrayTest.inc" role="test" />
@@ -2092,6 +2094,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
20922094
<install as="CodeSniffer/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" name="tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" />
20932095
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
20942096
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
2097+
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.php" name="tests/Core/Tokenizer/ReadonlyTest.php" />
2098+
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.inc" name="tests/Core/Tokenizer/ReadonlyTest.inc" />
20952099
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
20962100
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
20972101
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
@@ -2182,6 +2186,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
21822186
<install as="CodeSniffer/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" name="tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" />
21832187
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
21842188
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
2189+
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.php" name="tests/Core/Tokenizer/ReadonlyTest.php" />
2190+
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.inc" name="tests/Core/Tokenizer/ReadonlyTest.inc" />
21852191
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
21862192
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
21872193
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />

src/Tokenizers/PHP.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ class PHP extends Tokenizer
393393
T_PRIVATE => 7,
394394
T_PUBLIC => 6,
395395
T_PROTECTED => 9,
396+
T_READONLY => 8,
396397
T_REQUIRE => 7,
397398
T_REQUIRE_ONCE => 12,
398399
T_RETURN => 6,
@@ -2812,6 +2813,72 @@ protected function processAdditional()
28122813
$this->tokens[$x]['code'] = T_STRING;
28132814
$this->tokens[$x]['type'] = 'T_STRING';
28142815
}
2816+
} else if (($this->tokens[$i]['code'] === T_STRING && strtolower($this->tokens[$i]['content']) === 'readonly')
2817+
|| $this->tokens[$i]['code'] === T_READONLY
2818+
) {
2819+
/*
2820+
"readonly" keyword support
2821+
PHP < 8.1: Converts T_STRING to T_READONLY
2822+
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
2823+
*/
2824+
2825+
$allowedAfter = [
2826+
T_STRING => T_STRING,
2827+
T_NS_SEPARATOR => T_NS_SEPARATOR,
2828+
T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED,
2829+
T_NAME_RELATIVE => T_NAME_RELATIVE,
2830+
T_NAME_QUALIFIED => T_NAME_QUALIFIED,
2831+
T_TYPE_UNION => T_TYPE_UNION,
2832+
T_BITWISE_OR => T_BITWISE_OR,
2833+
T_ARRAY => T_ARRAY,
2834+
T_CALLABLE => T_CALLABLE,
2835+
T_SELF => T_SELF,
2836+
T_PARENT => T_PARENT,
2837+
T_NULL => T_FALSE,
2838+
T_NULLABLE => T_NULLABLE,
2839+
T_STATIC => T_STATIC,
2840+
T_PUBLIC => T_PUBLIC,
2841+
T_PROTECTED => T_PROTECTED,
2842+
T_PRIVATE => T_PRIVATE,
2843+
T_VAR => T_VAR,
2844+
];
2845+
2846+
$shouldBeReadonly = true;
2847+
2848+
for ($x = ($i + 1); $x < $numTokens; $x++) {
2849+
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
2850+
continue;
2851+
}
2852+
2853+
if ($this->tokens[$x]['code'] === T_VARIABLE) {
2854+
break;
2855+
}
2856+
2857+
if (isset($allowedAfter[$this->tokens[$x]['code']]) === false) {
2858+
$shouldBeReadonly = false;
2859+
break;
2860+
}
2861+
}
2862+
2863+
if ($this->tokens[$i]['code'] === T_STRING && $shouldBeReadonly === true) {
2864+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2865+
$line = $this->tokens[$i]['line'];
2866+
echo "\t* token $i on line $line changed from T_STRING to T_READONLY".PHP_EOL;
2867+
}
2868+
2869+
$this->tokens[$i]['code'] = T_READONLY;
2870+
$this->tokens[$i]['type'] = 'T_READONLY';
2871+
} else if ($this->tokens[$i]['code'] === T_READONLY && $shouldBeReadonly === false) {
2872+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2873+
$line = $this->tokens[$i]['line'];
2874+
echo "\t* token $i on line $line changed from T_READONLY to T_STRING".PHP_EOL;
2875+
}
2876+
2877+
$this->tokens[$i]['code'] = T_STRING;
2878+
$this->tokens[$i]['type'] = 'T_STRING';
2879+
}
2880+
2881+
continue;
28152882
}//end if
28162883

28172884
if (($this->tokens[$i]['code'] !== T_CASE

src/Util/Tokens.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@
163163
define('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG', 'PHPCS_T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG');
164164
}
165165

166+
if (defined('T_READONLY') === false) {
167+
define('T_READONLY', 'PHPCS_T_READONLY');
168+
}
169+
166170
// Tokens used for parsing doc blocks.
167171
define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR');
168172
define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE');

tests/Core/Tokenizer/ReadonlyTest.inc

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
class Foo
4+
{
5+
/* testReadonlyProperty */
6+
readonly int $readonlyProperty;
7+
/* testVarReadonlyProperty */
8+
var readonly int $varReadonlyProperty;
9+
/* testReadonlyVarProperty */
10+
readonly var int $testReadonlyVarProperty;
11+
/* testStaticReadonlyProperty */
12+
static readonly int $staticReadonlyProperty;
13+
/* testReadonlyStaticProperty */
14+
readonly static int $readonlyStaticProperty;
15+
/* testReadonlyPropertyWithoutType */
16+
readonly $propertyWithoutType;
17+
/* testPublicReadonlyProperty */
18+
public readonly int $publicReadonlyProperty;
19+
/* testProtectedReadonlyProperty */
20+
protected readonly int $protectedReadonlyProperty;
21+
/* testPrivateReadonlyProperty */
22+
private readonly int $privateReadonlyProperty;
23+
/* testPublicReadonlyPropertyWithReadonlyFirst */
24+
readonly public int $publicReadonlyProperty;
25+
/* testProtectedReadonlyPropertyWithReadonlyFirst */
26+
readonly protected int $protectedReadonlyProperty;
27+
/* testPrivateReadonlyPropertyWithReadonlyFirst */
28+
readonly private int $privateReadonlyProperty;
29+
/* testReadonlyWithCommentsInDeclaration */
30+
private /* Comment */ readonly /* Comment */ int /* Comment */ $readonlyPropertyWithCommentsInDeclaration;
31+
/* testReadonlyWithNullableProperty */
32+
private readonly ?int $nullableProperty;
33+
/* testReadonlyNullablePropertyWithUnionTypeHintAndNullFirst */
34+
private readonly null|int $nullablePropertyWithUnionTypeHintAndNullFirst;
35+
/* testReadonlyNullablePropertyWithUnionTypeHintAndNullLast */
36+
private readonly int|null $nullablePropertyWithUnionTypeHintAndNullLast;
37+
/* testReadonlyPropertyWithArrayTypeHint */
38+
private readonly array $arrayProperty;
39+
/* testReadonlyPropertyWithSelfTypeHint */
40+
private readonly self $selfProperty;
41+
/* testReadonlyPropertyWithParentTypeHint */
42+
private readonly parent $parentProperty;
43+
/* testReadonlyPropertyWithFullyQualifiedTypeHint */
44+
private readonly \stdClass $propertyWithFullyQualifiedTypeHint;
45+
46+
/* testReadonlyIsCaseInsensitive */
47+
public ReAdOnLy string $caseInsensitiveProperty;
48+
49+
/* testReadonlyConstructorPropertyPromotion */
50+
public function __construct(private readonly bool $constructorPropertyPromotion)
51+
{
52+
}
53+
}
54+
55+
$anonymousClass = new class () {
56+
/* testReadonlyPropertyInAnonymousClass */
57+
public readonly int $property;
58+
};
59+
60+
class ClassName {
61+
/* testReadonlyUsedAsClassConstantName */
62+
const READONLY = 'readonly';
63+
64+
/* testReadonlyUsedAsMethodName */
65+
public function readonly() {
66+
// Do something.
67+
68+
/* testReadonlyUsedAsPropertyName */
69+
$this->readonly = 'foo';
70+
71+
/* testReadonlyPropertyInTernaryOperator */
72+
$isReadonly = $this->readonly ? true : false;
73+
}
74+
}
75+
76+
/* testReadonlyUsedAsFunctionName */
77+
function readonly()
78+
{
79+
}
80+
81+
/* testReadonlyUsedAsNamespaceName */
82+
namespace Readonly;
83+
/* testReadonlyUsedAsPartOfNamespaceName */
84+
namespace My\Readonly\Collection;
85+
/* testReadonlyAsFunctionCall */
86+
$var = readonly($a, $b);
87+
/* testClassConstantFetchWithReadonlyAsConstantName */
88+
echo ClassName::READONLY;
89+
90+
/* testParseErrorLiveCoding */
91+
// This must be the last test in the file.
92+
readonly

0 commit comments

Comments
 (0)