Skip to content

Commit 1858e46

Browse files
jrfnlfredden
andcommitted
Fix mis-identification of 'readonly' keyword
PHP 8.2 introduces disjunctive normal form types, which use parentheses, which invalidates the previous "special casing" for function/method declarations and calls using the `readonly` keyword. This commit fixes this. Note: this does not (yet) add support for DNF types to the tokenizer or anywhere else in PHPCS, it only fixes the tokenization of `readonly`. Includes additional tests. Ref: php/php-src@08b7539 Co-authored-by: Dan Wallis <[email protected]>
1 parent 8f43103 commit 1858e46

File tree

3 files changed

+163
-6
lines changed

3 files changed

+163
-6
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,42 @@ echo ClassName::READONLY;
102102
/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */
103103
$var = readonly /* comment */ ();
104104

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+
105141
/* testParseErrorLiveCoding */
106142
// This must be the last test in the file.
107143
readonly

tests/Core/Tokenizer/BackfillReadonlyTest.php

Lines changed: 41 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
[
@@ -252,6 +284,14 @@ public function dataNotReadonly()
252284
'/* testClassConstantFetchWithReadonlyAsConstantName */',
253285
'READONLY',
254286
],
287+
[
288+
'/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */',
289+
'readonly',
290+
],
291+
[
292+
'/* testReadonlyUsedAsMethodNameWithDNFParam */',
293+
'readonly',
294+
],
255295
];
256296

257297
}//end dataNotReadonly()

0 commit comments

Comments
 (0)