Skip to content

Commit 27d3ec8

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 cd6a27d commit 27d3ec8

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
@@ -1247,32 +1247,113 @@ protected function tokenize($string)
12471247
"readonly" keyword for PHP < 8.1
12481248
*/
12491249

1250-
if (PHP_VERSION_ID < 80100
1251-
&& $tokenIsArray === true
1250+
if ($tokenIsArray === true
12521251
&& strtolower($token[1]) === 'readonly'
12531252
&& isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
12541253
) {
12551254
// Get the next non-whitespace token.
12561255
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
12571256
if (is_array($tokens[$i]) === false
1258-
|| $tokens[$i][0] !== T_WHITESPACE
1257+
|| isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
12591258
) {
12601259
break;
12611260
}
12621261
}
12631262

1263+
$isReadonlyKeyword = false;
1264+
12641265
if (isset($tokens[$i]) === false
12651266
|| $tokens[$i] !== '('
12661267
) {
1268+
$isReadonlyKeyword = true;
1269+
} else if ($tokens[$i] === '(') {
1270+
/*
1271+
* Skip over tokens which can be used in type declarations.
1272+
* At this point, the only token types which need to be taken into consideration
1273+
* as potential type declarations are identifier names, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR
1274+
* and the union/intersection/dnf parentheses.
1275+
*/
1276+
1277+
$foundDNFParens = 1;
1278+
$foundDNFPipe = 0;
1279+
1280+
for (++$i; $i < $numTokens; $i++) {
1281+
if (is_array($tokens[$i]) === true) {
1282+
$tokenType = $tokens[$i][0];
1283+
} else {
1284+
$tokenType = $tokens[$i];
1285+
}
1286+
1287+
if (isset(Util\Tokens::$emptyTokens[$tokenType]) === true) {
1288+
continue;
1289+
}
1290+
1291+
if ($tokenType === '|') {
1292+
++$foundDNFPipe;
1293+
continue;
1294+
}
1295+
1296+
if ($tokenType === ')') {
1297+
++$foundDNFParens;
1298+
continue;
1299+
}
1300+
1301+
if ($tokenType === '(') {
1302+
++$foundDNFParens;
1303+
continue;
1304+
}
1305+
1306+
if ($tokenType === T_STRING
1307+
|| $tokenType === T_NAME_FULLY_QUALIFIED
1308+
|| $tokenType === T_NAME_RELATIVE
1309+
|| $tokenType === T_NAME_QUALIFIED
1310+
|| $tokenType === T_ARRAY
1311+
|| $tokenType === T_NAMESPACE
1312+
|| $tokenType === T_NS_SEPARATOR
1313+
|| $tokenType === T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG // PHP 8.0+.
1314+
|| $tokenType === '&' // PHP < 8.0.
1315+
) {
1316+
continue;
1317+
}
1318+
1319+
// Reached the next token after.
1320+
if (($foundDNFParens % 2) === 0
1321+
&& $foundDNFPipe >= 1
1322+
&& ($tokenType === T_VARIABLE
1323+
|| $tokenType === T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG)
1324+
) {
1325+
$isReadonlyKeyword = true;
1326+
}
1327+
1328+
break;
1329+
}//end for
1330+
}//end if
1331+
1332+
if ($isReadonlyKeyword === true) {
12671333
$finalTokens[$newStackPtr] = [
12681334
'code' => T_READONLY,
12691335
'type' => 'T_READONLY',
12701336
'content' => $token[1],
12711337
];
12721338
$newStackPtr++;
12731339

1274-
continue;
1275-
}
1340+
if (PHP_CODESNIFFER_VERBOSITY > 1 && $type !== T_READONLY) {
1341+
echo "\t\t* token $stackPtr changed from $type to T_READONLY".PHP_EOL;
1342+
}
1343+
} else {
1344+
$finalTokens[$newStackPtr] = [
1345+
'code' => T_STRING,
1346+
'type' => 'T_STRING',
1347+
'content' => $token[1],
1348+
];
1349+
$newStackPtr++;
1350+
1351+
if (PHP_CODESNIFFER_VERBOSITY > 1 && $type !== T_STRING) {
1352+
echo "\t\t* token $stackPtr changed from $type to T_STRING".PHP_EOL;
1353+
}
1354+
}//end if
1355+
1356+
continue;
12761357
}//end if
12771358

12781359
/*

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
[
@@ -254,6 +286,14 @@ public function dataNotReadonly()
254286
'/* testClassConstantFetchWithReadonlyAsConstantName */',
255287
'READONLY',
256288
],
289+
[
290+
'/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */',
291+
'readonly',
292+
],
293+
[
294+
'/* testReadonlyUsedAsMethodNameWithDNFParam */',
295+
'readonly',
296+
],
257297
];
258298

259299
}//end dataNotReadonly()

0 commit comments

Comments
 (0)