Skip to content

Commit 1f140ab

Browse files
committed
PHP 8.1 | PSR2/PropertyDeclaration: add check for visibility before readonly keyword
The `PSR2.Classes.PropertyDeclaration` sniff includes a check to verify that the `static` keyword is _after_ the visibility in a property declaration. PHP 8.1 introduced the `readonly` keyword for properties, but as PSR2/PSR12 were both released before PHP 8.1, no PSR rules have (yet) been defined for the modifier keyword order for `readonly` properties. Having said that, all code samples in both the RFC as well as the PHP manual use the `visibility - readonly` modifier keyword order, so it is likely that this will become the standard modifier keyword order. A search for PHP projects which have started to use the `readonly` keyword, also shows these predominantly use the `visibility - readonly` modifier keyword order. With that in mind, I'm proposing to add a check for this order to the `PSR2.Classes.PropertyDeclaration` sniff - this being the only PHPCS native sniff which checks the modifier keyword order for properties. As PSR2/PSR12 do not formally have rules for the order (yet), the new error code being introduced is excluded for those rulesets (for the time being). Includes unit tests. Ref: * https://wiki.php.net/rfc/readonly_properties_v2 * https://www.php.net/manual/en/language.oop5.properties.php#language.oop5.properties.readonly-properties * https://sourcegraph.com/search?q=context:global+%5Cs%28public%7Cprotected%7Cprivate%29%5Cs%2Breadonly%5Cs%2B+lang:php+-file:%28%5E%7C/%29%28vendor%7Ctests%3F%29/+fork:no+archived:no&patternType=regexp (searching `visibility - readonly` order - > 500 results) * https://sourcegraph.com/search?q=context:global+%5Csreadonly%5Cs%2B%28public%7Cprotected%7Cprivate%29%5Cs%2B+lang:php+-file:%28%5E%7C/%29%28vendor%7Ctests%3F%29/+fork:no+archived:no&patternType=regexp (searching `readonly - visibility` order - 41 (non false positive) results)
1 parent ed8e00d commit 1f140ab

File tree

6 files changed

+53
-14
lines changed

6 files changed

+53
-14
lines changed

src/Standards/PSR12/ruleset.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@
143143
That is, an underscore prefix explicitly has no meaning. -->
144144
<!-- There MUST be a space between type declaration and property name. -->
145145
<rule ref="PSR2.Classes.PropertyDeclaration"/>
146+
<rule ref="PSR2.Classes.PropertyDeclaration.ReadonlyBeforeVisibility">
147+
<severity>0</severity>
148+
</rule>
146149

147150
<!-- Visibility MUST be declared on all constants if your project PHP minimum version supports constant visibilities (PHP 7.1 or later). -->
148151
<!-- checked by PSR12.Properties.ConstantVisibility -->

src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,27 +121,50 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
121121
if ($propertyInfo['scope_specified'] === true && $propertyInfo['is_static'] === true) {
122122
$scopePtr = $phpcsFile->findPrevious(Tokens::$scopeModifiers, ($stackPtr - 1));
123123
$staticPtr = $phpcsFile->findPrevious(T_STATIC, ($stackPtr - 1));
124-
if ($scopePtr < $staticPtr) {
125-
return;
126-
}
124+
if ($scopePtr > $staticPtr) {
125+
$error = 'The static declaration must come after the visibility declaration';
126+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'StaticBeforeVisibility');
127+
if ($fix === true) {
128+
$phpcsFile->fixer->beginChangeset();
127129

128-
$error = 'The static declaration must come after the visibility declaration';
129-
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'StaticBeforeVisibility');
130-
if ($fix === true) {
131-
$phpcsFile->fixer->beginChangeset();
130+
for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) {
131+
if ($tokens[$i]['code'] !== T_WHITESPACE) {
132+
break;
133+
}
132134

133-
for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) {
134-
if ($tokens[$i]['code'] !== T_WHITESPACE) {
135-
break;
135+
$phpcsFile->fixer->replaceToken($i, '');
136136
}
137137

138-
$phpcsFile->fixer->replaceToken($i, '');
138+
$phpcsFile->fixer->replaceToken($scopePtr, '');
139+
$phpcsFile->fixer->addContentBefore($staticPtr, $propertyInfo['scope'].' ');
140+
141+
$phpcsFile->fixer->endChangeset();
139142
}
143+
}
144+
}//end if
140145

141-
$phpcsFile->fixer->replaceToken($scopePtr, '');
142-
$phpcsFile->fixer->addContentBefore($staticPtr, $propertyInfo['scope'].' ');
146+
if ($propertyInfo['scope_specified'] === true && $propertyInfo['is_readonly'] === true) {
147+
$scopePtr = $phpcsFile->findPrevious(Tokens::$scopeModifiers, ($stackPtr - 1));
148+
$readonlyPtr = $phpcsFile->findPrevious(T_READONLY, ($stackPtr - 1));
149+
if ($scopePtr > $readonlyPtr) {
150+
$error = 'The readonly declaration must come after the visibility declaration';
151+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'ReadonlyBeforeVisibility');
152+
if ($fix === true) {
153+
$phpcsFile->fixer->beginChangeset();
154+
155+
for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) {
156+
if ($tokens[$i]['code'] !== T_WHITESPACE) {
157+
break;
158+
}
143159

144-
$phpcsFile->fixer->endChangeset();
160+
$phpcsFile->fixer->replaceToken($i, '');
161+
}
162+
163+
$phpcsFile->fixer->replaceToken($scopePtr, '');
164+
$phpcsFile->fixer->addContentBefore($readonlyPtr, $propertyInfo['scope'].' ');
165+
166+
$phpcsFile->fixer->endChangeset();
167+
}
145168
}
146169
}//end if
147170

src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,8 @@ class ReadOnlyProp {
8080
protected readonly ?string $foo;
8181

8282
readonly array $foo;
83+
84+
readonly public int $wrongOrder1;
85+
86+
readonly protected ?string $wrongOrder2;
8387
}

src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,8 @@ class ReadOnlyProp {
7777
protected readonly ?string $foo;
7878

7979
readonly array $foo;
80+
81+
public readonly int $wrongOrder1;
82+
83+
protected readonly ?string $wrongOrder2;
8084
}

src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public function getErrorList()
4949
76 => 1,
5050
80 => 1,
5151
82 => 1,
52+
84 => 1,
53+
86 => 1,
5254
];
5355

5456
}//end getErrorList()

src/Standards/PSR2/ruleset.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@
104104
<rule ref="PSR2.Classes.PropertyDeclaration.SpacingAfterType">
105105
<severity>0</severity>
106106
</rule>
107+
<rule ref="PSR2.Classes.PropertyDeclaration.ReadonlyBeforeVisibility">
108+
<severity>0</severity>
109+
</rule>
107110

108111
<!-- 4.3 Methods -->
109112

0 commit comments

Comments
 (0)