Skip to content

Commit 92a9697

Browse files
authored
Merge pull request #34 from PHPCSStandards/fredden/php82-readonly-keyword-as-function-call
Tokenizer/PHP: fix mis-identification of 'readonly' keyword icw PHP 8.2 DNF types
2 parents 7b103ed + 1858e46 commit 92a9697

File tree

3 files changed

+202
-14
lines changed

3 files changed

+202
-14
lines changed

src/Tokenizers/PHP.php

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,32 +1313,113 @@ protected function tokenize($string)
13131313
"readonly" keyword for PHP < 8.1
13141314
*/
13151315

1316-
if (PHP_VERSION_ID < 80100
1317-
&& $tokenIsArray === true
1316+
if ($tokenIsArray === true
13181317
&& strtolower($token[1]) === 'readonly'
13191318
&& isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
13201319
) {
13211320
// Get the next non-whitespace token.
13221321
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
13231322
if (is_array($tokens[$i]) === false
1324-
|| $tokens[$i][0] !== T_WHITESPACE
1323+
|| isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
13251324
) {
13261325
break;
13271326
}
13281327
}
13291328

1329+
$isReadonlyKeyword = false;
1330+
13301331
if (isset($tokens[$i]) === false
13311332
|| $tokens[$i] !== '('
13321333
) {
1334+
$isReadonlyKeyword = true;
1335+
} else if ($tokens[$i] === '(') {
1336+
/*
1337+
* Skip over tokens which can be used in type declarations.
1338+
* At this point, the only token types which need to be taken into consideration
1339+
* as potential type declarations are identifier names, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR
1340+
* and the union/intersection/dnf parentheses.
1341+
*/
1342+
1343+
$foundDNFParens = 1;
1344+
$foundDNFPipe = 0;
1345+
1346+
for (++$i; $i < $numTokens; $i++) {
1347+
if (is_array($tokens[$i]) === true) {
1348+
$tokenType = $tokens[$i][0];
1349+
} else {
1350+
$tokenType = $tokens[$i];
1351+
}
1352+
1353+
if (isset(Util\Tokens::$emptyTokens[$tokenType]) === true) {
1354+
continue;
1355+
}
1356+
1357+
if ($tokenType === '|') {
1358+
++$foundDNFPipe;
1359+
continue;
1360+
}
1361+
1362+
if ($tokenType === ')') {
1363+
++$foundDNFParens;
1364+
continue;
1365+
}
1366+
1367+
if ($tokenType === '(') {
1368+
++$foundDNFParens;
1369+
continue;
1370+
}
1371+
1372+
if ($tokenType === T_STRING
1373+
|| $tokenType === T_NAME_FULLY_QUALIFIED
1374+
|| $tokenType === T_NAME_RELATIVE
1375+
|| $tokenType === T_NAME_QUALIFIED
1376+
|| $tokenType === T_ARRAY
1377+
|| $tokenType === T_NAMESPACE
1378+
|| $tokenType === T_NS_SEPARATOR
1379+
|| $tokenType === T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG // PHP 8.0+.
1380+
|| $tokenType === '&' // PHP < 8.0.
1381+
) {
1382+
continue;
1383+
}
1384+
1385+
// Reached the next token after.
1386+
if (($foundDNFParens % 2) === 0
1387+
&& $foundDNFPipe >= 1
1388+
&& ($tokenType === T_VARIABLE
1389+
|| $tokenType === T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG)
1390+
) {
1391+
$isReadonlyKeyword = true;
1392+
}
1393+
1394+
break;
1395+
}//end for
1396+
}//end if
1397+
1398+
if ($isReadonlyKeyword === true) {
13331399
$finalTokens[$newStackPtr] = [
13341400
'code' => T_READONLY,
13351401
'type' => 'T_READONLY',
13361402
'content' => $token[1],
13371403
];
13381404
$newStackPtr++;
13391405

1340-
continue;
1341-
}
1406+
if (PHP_CODESNIFFER_VERBOSITY > 1 && $type !== T_READONLY) {
1407+
echo "\t\t* token $stackPtr changed from $type to T_READONLY".PHP_EOL;
1408+
}
1409+
} else {
1410+
$finalTokens[$newStackPtr] = [
1411+
'code' => T_STRING,
1412+
'type' => 'T_STRING',
1413+
'content' => $token[1],
1414+
];
1415+
$newStackPtr++;
1416+
1417+
if (PHP_CODESNIFFER_VERBOSITY > 1 && $type !== T_STRING) {
1418+
echo "\t\t* token $stackPtr changed from $type to T_STRING".PHP_EOL;
1419+
}
1420+
}//end if
1421+
1422+
continue;
13421423
}//end if
13431424

13441425
/*

tests/Core/Tokenizer/BackfillReadonlyTest.inc

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ class Foo
4949
public ReAdOnLy string $caseInsensitiveProperty;
5050

5151
/* testReadonlyConstructorPropertyPromotion */
52-
public function __construct(private readonly bool $constructorPropertyPromotion)
53-
{
54-
}
52+
public function __construct(private readonly bool $constructorPropertyPromotion) {}
5553

5654
/* testReadonlyConstructorPropertyPromotionWithReference */
5755
public function __construct(private ReadOnly bool &$constructorPropertyPromotion) {}
@@ -68,8 +66,6 @@ class ClassName {
6866

6967
/* testReadonlyUsedAsMethodName */
7068
public function readonly() {
71-
// Do something.
72-
7369
/* testReadonlyUsedAsPropertyName */
7470
$this->readonly = 'foo';
7571

@@ -79,22 +75,69 @@ class ClassName {
7975
}
8076

8177
/* testReadonlyUsedAsFunctionName */
82-
function readonly()
83-
{
84-
}
78+
function readonly() {}
79+
80+
/* testReadonlyUsedAsFunctionNameWithReturnByRef */
81+
function &readonly() {}
8582

8683
/* testReadonlyUsedAsNamespaceName */
8784
namespace Readonly;
8885
/* testReadonlyUsedAsPartOfNamespaceName */
8986
namespace My\Readonly\Collection;
9087
/* testReadonlyAsFunctionCall */
9188
$var = readonly($a, $b);
89+
/* testReadonlyAsNamespacedFunctionCall */
90+
$var = My\NS\readonly($a, $b);
91+
/* testReadonlyAsNamespaceRelativeFunctionCall */
92+
$var = namespace\ReadOnly($a, $b);
93+
/* testReadonlyAsMethodCall */
94+
$var = $obj->readonly($a, $b);
95+
/* testReadonlyAsNullsafeMethodCall */
96+
$var = $obj?->readOnly($a, $b);
97+
/* testReadonlyAsStaticMethodCallWithSpace */
98+
$var = ClassName::readonly ($a, $b);
9299
/* testClassConstantFetchWithReadonlyAsConstantName */
93100
echo ClassName::READONLY;
94101

95102
/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */
96103
$var = readonly /* comment */ ();
97104

105+
// These test cases are inspired by
106+
// https://github.com/php/php-src/commit/08b75395838b4b42a41e3c70684fa6c6b113eee0
107+
class ReadonlyWithDisjunctiveNormalForm
108+
{
109+
/* testReadonlyPropertyDNFTypeUnqualified */
110+
readonly (B&C)|A $h;
111+
112+
/* testReadonlyPropertyDNFTypeFullyQualified */
113+
public readonly (\Fully\Qualified\B&\Full\C)|\Foo\Bar $j;
114+
115+
/* testReadonlyPropertyDNFTypePartiallyQualified */
116+
protected readonly (Partially\Qualified&C)|A $l;
117+
118+
/* testReadonlyPropertyDNFTypeRelativeName */
119+
private readonly (namespace\Relative&C)|A $n;
120+
121+
/* testReadonlyPropertyDNFTypeMultipleSets */
122+
private readonly (A&C)|(B&C)|(C&D) $m;
123+
124+
/* testReadonlyPropertyDNFTypeWithArray */
125+
private readonly (B & C)|array $o;
126+
127+
/* testReadonlyPropertyDNFTypeWithSpacesAndComments */
128+
private readonly ( B & C /*something*/) | A $q;
129+
130+
public function __construct(
131+
/* testReadonlyConstructorPropertyPromotionWithDNF */
132+
private readonly (B&C)|A $b1,
133+
/* testReadonlyConstructorPropertyPromotionWithDNFAndRefence */
134+
readonly (B&C)|A &$b2,
135+
) {}
136+
137+
/* testReadonlyUsedAsMethodNameWithDNFParam */
138+
public function readonly (A&B $param): void {}
139+
}
140+
98141
/* testParseErrorLiveCoding */
99142
// This must be the last test in the file.
100143
readonly

tests/Core/Tokenizer/BackfillReadonlyTest.php

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,39 @@ public function dataReadonly()
148148
'readonly',
149149
],
150150
[
151-
'/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */',
151+
'/* testReadonlyPropertyDNFTypeUnqualified */',
152+
'readonly',
153+
],
154+
[
155+
'/* testReadonlyPropertyDNFTypeFullyQualified */',
156+
'readonly',
157+
],
158+
[
159+
'/* testReadonlyPropertyDNFTypePartiallyQualified */',
160+
'readonly',
161+
],
162+
[
163+
'/* testReadonlyPropertyDNFTypeRelativeName */',
164+
'readonly',
165+
],
166+
[
167+
'/* testReadonlyPropertyDNFTypeMultipleSets */',
168+
'readonly',
169+
],
170+
[
171+
'/* testReadonlyPropertyDNFTypeWithArray */',
172+
'readonly',
173+
],
174+
[
175+
'/* testReadonlyPropertyDNFTypeWithSpacesAndComments */',
176+
'readonly',
177+
],
178+
[
179+
'/* testReadonlyConstructorPropertyPromotionWithDNF */',
180+
'readonly',
181+
],
182+
[
183+
'/* testReadonlyConstructorPropertyPromotionWithDNFAndRefence */',
152184
'readonly',
153185
],
154186
[
@@ -212,6 +244,10 @@ public function dataNotReadonly()
212244
'/* testReadonlyUsedAsFunctionName */',
213245
'readonly',
214246
],
247+
[
248+
'/* testReadonlyUsedAsFunctionNameWithReturnByRef */',
249+
'readonly',
250+
],
215251
[
216252
'/* testReadonlyUsedAsNamespaceName */',
217253
'Readonly',
@@ -224,10 +260,38 @@ public function dataNotReadonly()
224260
'/* testReadonlyAsFunctionCall */',
225261
'readonly',
226262
],
263+
[
264+
'/* testReadonlyAsNamespacedFunctionCall */',
265+
'readonly',
266+
],
267+
[
268+
'/* testReadonlyAsNamespaceRelativeFunctionCall */',
269+
'ReadOnly',
270+
],
271+
[
272+
'/* testReadonlyAsMethodCall */',
273+
'readonly',
274+
],
275+
[
276+
'/* testReadonlyAsNullsafeMethodCall */',
277+
'readOnly',
278+
],
279+
[
280+
'/* testReadonlyAsStaticMethodCallWithSpace */',
281+
'readonly',
282+
],
227283
[
228284
'/* testClassConstantFetchWithReadonlyAsConstantName */',
229285
'READONLY',
230286
],
287+
[
288+
'/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */',
289+
'readonly',
290+
],
291+
[
292+
'/* testReadonlyUsedAsMethodNameWithDNFParam */',
293+
'readonly',
294+
],
231295
];
232296

233297
}//end dataNotReadonly()

0 commit comments

Comments
 (0)