Skip to content

Commit 21bd369

Browse files
committed
Merge branch 'php81-readonly' of https://github.com/kukulich/PHP_CodeSniffer
2 parents d6a58bd + e908da7 commit 21bd369

File tree

8 files changed

+426
-0
lines changed

8 files changed

+426
-0
lines changed

package.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
138138
<file baseinstalldir="" name="NamedFunctionCallArgumentsTest.php" role="test" />
139139
<file baseinstalldir="" name="NullsafeObjectOperatorTest.inc" role="test" />
140140
<file baseinstalldir="" name="NullsafeObjectOperatorTest.php" role="test" />
141+
<file baseinstalldir="" name="ReadonlyTest.inc" role="test" />
142+
<file baseinstalldir="" name="ReadonlyTest.php" role="test" />
141143
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.inc" role="test" />
142144
<file baseinstalldir="" name="ScopeSettingWithNamespaceOperatorTest.php" role="test" />
143145
<file baseinstalldir="" name="ShortArrayTest.inc" role="test" />
@@ -2095,6 +2097,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
20952097
<install as="CodeSniffer/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" name="tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" />
20962098
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
20972099
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
2100+
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.php" name="tests/Core/Tokenizer/ReadonlyTest.php" />
2101+
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.inc" name="tests/Core/Tokenizer/ReadonlyTest.inc" />
20982102
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
20992103
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
21002104
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />
@@ -2187,6 +2191,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
21872191
<install as="CodeSniffer/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" name="tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc" />
21882192
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.php" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.php" />
21892193
<install as="CodeSniffer/Core/Tokenizer/NullsafeObjectOperatorTest.inc" name="tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc" />
2194+
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.php" name="tests/Core/Tokenizer/ReadonlyTest.php" />
2195+
<install as="CodeSniffer/Core/Tokenizer/ReadonlyTest.inc" name="tests/Core/Tokenizer/ReadonlyTest.inc" />
21902196
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php" />
21912197
<install as="CodeSniffer/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" name="tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc" />
21922198
<install as="CodeSniffer/Core/Tokenizer/ShortArrayTest.php" name="tests/Core/Tokenizer/ShortArrayTest.php" />

src/Files/File.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1811,13 +1811,15 @@ public function getMemberProperties($stackPtr)
18111811
T_PROTECTED => T_PROTECTED,
18121812
T_STATIC => T_STATIC,
18131813
T_VAR => T_VAR,
1814+
T_READONLY => T_READONLY,
18141815
];
18151816

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

18181819
$scope = 'public';
18191820
$scopeSpecified = false;
18201821
$isStatic = false;
1822+
$isReadonly = false;
18211823

18221824
$startOfStatement = $this->findPrevious(
18231825
[
@@ -1850,6 +1852,9 @@ public function getMemberProperties($stackPtr)
18501852
case T_STATIC:
18511853
$isStatic = true;
18521854
break;
1855+
case T_READONLY:
1856+
$isReadonly = true;
1857+
break;
18531858
}
18541859
}//end for
18551860

@@ -1901,6 +1906,7 @@ public function getMemberProperties($stackPtr)
19011906
'scope' => $scope,
19021907
'scope_specified' => $scopeSpecified,
19031908
'is_static' => $isStatic,
1909+
'is_readonly' => $isReadonly,
19041910
'type' => $type,
19051911
'type_token' => $typeToken,
19061912
'type_end_token' => $typeEndToken,

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,
@@ -2836,6 +2837,72 @@ protected function processAdditional()
28362837
$this->tokens[$x]['code'] = T_STRING;
28372838
$this->tokens[$x]['type'] = 'T_STRING';
28382839
}
2840+
} else if (($this->tokens[$i]['code'] === T_STRING && strtolower($this->tokens[$i]['content']) === 'readonly')
2841+
|| $this->tokens[$i]['code'] === T_READONLY
2842+
) {
2843+
/*
2844+
"readonly" keyword support
2845+
PHP < 8.1: Converts T_STRING to T_READONLY
2846+
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
2847+
*/
2848+
2849+
$allowedAfter = [
2850+
T_STRING => T_STRING,
2851+
T_NS_SEPARATOR => T_NS_SEPARATOR,
2852+
T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED,
2853+
T_NAME_RELATIVE => T_NAME_RELATIVE,
2854+
T_NAME_QUALIFIED => T_NAME_QUALIFIED,
2855+
T_TYPE_UNION => T_TYPE_UNION,
2856+
T_BITWISE_OR => T_BITWISE_OR,
2857+
T_ARRAY => T_ARRAY,
2858+
T_CALLABLE => T_CALLABLE,
2859+
T_SELF => T_SELF,
2860+
T_PARENT => T_PARENT,
2861+
T_NULL => T_FALSE,
2862+
T_NULLABLE => T_NULLABLE,
2863+
T_STATIC => T_STATIC,
2864+
T_PUBLIC => T_PUBLIC,
2865+
T_PROTECTED => T_PROTECTED,
2866+
T_PRIVATE => T_PRIVATE,
2867+
T_VAR => T_VAR,
2868+
];
2869+
2870+
$shouldBeReadonly = true;
2871+
2872+
for ($x = ($i + 1); $x < $numTokens; $x++) {
2873+
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
2874+
continue;
2875+
}
2876+
2877+
if ($this->tokens[$x]['code'] === T_VARIABLE) {
2878+
break;
2879+
}
2880+
2881+
if (isset($allowedAfter[$this->tokens[$x]['code']]) === false) {
2882+
$shouldBeReadonly = false;
2883+
break;
2884+
}
2885+
}
2886+
2887+
if ($this->tokens[$i]['code'] === T_STRING && $shouldBeReadonly === true) {
2888+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2889+
$line = $this->tokens[$i]['line'];
2890+
echo "\t* token $i on line $line changed from T_STRING to T_READONLY".PHP_EOL;
2891+
}
2892+
2893+
$this->tokens[$i]['code'] = T_READONLY;
2894+
$this->tokens[$i]['type'] = 'T_READONLY';
2895+
} else if ($this->tokens[$i]['code'] === T_READONLY && $shouldBeReadonly === false) {
2896+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2897+
$line = $this->tokens[$i]['line'];
2898+
echo "\t* token $i on line $line changed from T_READONLY to T_STRING".PHP_EOL;
2899+
}
2900+
2901+
$this->tokens[$i]['code'] = T_STRING;
2902+
$this->tokens[$i]['type'] = 'T_STRING';
2903+
}
2904+
2905+
continue;
28392906
}//end if
28402907

28412908
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/File/GetMemberPropertiesTest.inc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ $anon = class() {
239239
/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */
240240
// Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method.
241241
public int |string| /*comment*/ INT $duplicateTypeInUnion;
242+
243+
/* testPHP81NotReadonly */
244+
private string $notReadonly;
245+
/* testPHP81Readonly */
246+
public readonly int $readonly;
242247
};
243248

244249
$anon = class {

tests/Core/File/GetMemberPropertiesTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,28 @@ public function dataGetMemberProperties()
610610
'nullable_type' => false,
611611
],
612612
],
613+
[
614+
'/* testPHP81NotReadonly */',
615+
[
616+
'scope' => 'private',
617+
'scope_specified' => true,
618+
'is_static' => false,
619+
'is_readonly' => false,
620+
'type' => 'string',
621+
'nullable_type' => false,
622+
],
623+
],
624+
[
625+
'/* testPHP81Readonly */',
626+
[
627+
'scope' => 'public',
628+
'scope_specified' => true,
629+
'is_static' => false,
630+
'is_readonly' => true,
631+
'type' => 'int',
632+
'nullable_type' => false,
633+
],
634+
],
613635
[
614636
'/* testPHP8PropertySingleAttribute */',
615637
[

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)