diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 42ba192e91..badc9d972b 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -1313,23 +1313,89 @@ protected function tokenize($string) "readonly" keyword for PHP < 8.1 */ - if (PHP_VERSION_ID < 80100 - && $tokenIsArray === true + if ($tokenIsArray === true && strtolower($token[1]) === 'readonly' && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false ) { // Get the next non-whitespace token. for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { if (is_array($tokens[$i]) === false - || $tokens[$i][0] !== T_WHITESPACE + || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false ) { break; } } + $isReadonlyKeyword = false; + if (isset($tokens[$i]) === false || $tokens[$i] !== '(' ) { + $isReadonlyKeyword = true; + } else if ($tokens[$i] === '(') { + /* + * Skip over tokens which can be used in type declarations. + * At this point, the only token types which need to be taken into consideration + * as potential type declarations are identifier names, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR + * and the union/intersection/dnf parentheses. + */ + + $foundDNFParens = 1; + $foundDNFPipe = 0; + + for (++$i; $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === true) { + $tokenType = $tokens[$i][0]; + } else { + $tokenType = $tokens[$i]; + } + + if (isset(Util\Tokens::$emptyTokens[$tokenType]) === true) { + continue; + } + + if ($tokenType === '|') { + ++$foundDNFPipe; + continue; + } + + if ($tokenType === ')') { + ++$foundDNFParens; + continue; + } + + if ($tokenType === '(') { + ++$foundDNFParens; + continue; + } + + if ($tokenType === T_STRING + || $tokenType === T_NAME_FULLY_QUALIFIED + || $tokenType === T_NAME_RELATIVE + || $tokenType === T_NAME_QUALIFIED + || $tokenType === T_ARRAY + || $tokenType === T_NAMESPACE + || $tokenType === T_NS_SEPARATOR + || $tokenType === T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG // PHP 8.0+. + || $tokenType === '&' // PHP < 8.0. + ) { + continue; + } + + // Reached the next token after. + if (($foundDNFParens % 2) === 0 + && $foundDNFPipe >= 1 + && ($tokenType === T_VARIABLE + || $tokenType === T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG) + ) { + $isReadonlyKeyword = true; + } + + break; + }//end for + }//end if + + if ($isReadonlyKeyword === true) { $finalTokens[$newStackPtr] = [ 'code' => T_READONLY, 'type' => 'T_READONLY', @@ -1337,8 +1403,23 @@ protected function tokenize($string) ]; $newStackPtr++; - continue; - } + if (PHP_CODESNIFFER_VERBOSITY > 1 && $type !== T_READONLY) { + echo "\t\t* token $stackPtr changed from $type to T_READONLY".PHP_EOL; + } + } else { + $finalTokens[$newStackPtr] = [ + 'code' => T_STRING, + 'type' => 'T_STRING', + 'content' => $token[1], + ]; + $newStackPtr++; + + if (PHP_CODESNIFFER_VERBOSITY > 1 && $type !== T_STRING) { + echo "\t\t* token $stackPtr changed from $type to T_STRING".PHP_EOL; + } + }//end if + + continue; }//end if /* diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.inc b/tests/Core/Tokenizer/BackfillReadonlyTest.inc index 1e8e59a3b7..1e66d4643b 100644 --- a/tests/Core/Tokenizer/BackfillReadonlyTest.inc +++ b/tests/Core/Tokenizer/BackfillReadonlyTest.inc @@ -102,6 +102,42 @@ echo ClassName::READONLY; /* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */ $var = readonly /* comment */ (); +// These test cases are inspired by +// https://github.com/php/php-src/commit/08b75395838b4b42a41e3c70684fa6c6b113eee0 +class ReadonlyWithDisjunctiveNormalForm +{ + /* testReadonlyPropertyDNFTypeUnqualified */ + readonly (B&C)|A $h; + + /* testReadonlyPropertyDNFTypeFullyQualified */ + public readonly (\Fully\Qualified\B&\Full\C)|\Foo\Bar $j; + + /* testReadonlyPropertyDNFTypePartiallyQualified */ + protected readonly (Partially\Qualified&C)|A $l; + + /* testReadonlyPropertyDNFTypeRelativeName */ + private readonly (namespace\Relative&C)|A $n; + + /* testReadonlyPropertyDNFTypeMultipleSets */ + private readonly (A&C)|(B&C)|(C&D) $m; + + /* testReadonlyPropertyDNFTypeWithArray */ + private readonly (B & C)|array $o; + + /* testReadonlyPropertyDNFTypeWithSpacesAndComments */ + private readonly ( B & C /*something*/) | A $q; + + public function __construct( + /* testReadonlyConstructorPropertyPromotionWithDNF */ + private readonly (B&C)|A $b1, + /* testReadonlyConstructorPropertyPromotionWithDNFAndRefence */ + readonly (B&C)|A &$b2, + ) {} + + /* testReadonlyUsedAsMethodNameWithDNFParam */ + public function readonly (A&B $param): void {} +} + /* testParseErrorLiveCoding */ // This must be the last test in the file. readonly diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.php b/tests/Core/Tokenizer/BackfillReadonlyTest.php index 021dc17db1..024220d3bf 100644 --- a/tests/Core/Tokenizer/BackfillReadonlyTest.php +++ b/tests/Core/Tokenizer/BackfillReadonlyTest.php @@ -148,7 +148,39 @@ public function dataReadonly() 'readonly', ], [ - '/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */', + '/* testReadonlyPropertyDNFTypeUnqualified */', + 'readonly', + ], + [ + '/* testReadonlyPropertyDNFTypeFullyQualified */', + 'readonly', + ], + [ + '/* testReadonlyPropertyDNFTypePartiallyQualified */', + 'readonly', + ], + [ + '/* testReadonlyPropertyDNFTypeRelativeName */', + 'readonly', + ], + [ + '/* testReadonlyPropertyDNFTypeMultipleSets */', + 'readonly', + ], + [ + '/* testReadonlyPropertyDNFTypeWithArray */', + 'readonly', + ], + [ + '/* testReadonlyPropertyDNFTypeWithSpacesAndComments */', + 'readonly', + ], + [ + '/* testReadonlyConstructorPropertyPromotionWithDNF */', + 'readonly', + ], + [ + '/* testReadonlyConstructorPropertyPromotionWithDNFAndRefence */', 'readonly', ], [ @@ -252,6 +284,14 @@ public function dataNotReadonly() '/* testClassConstantFetchWithReadonlyAsConstantName */', 'READONLY', ], + [ + '/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsMethodNameWithDNFParam */', + 'readonly', + ], ]; }//end dataNotReadonly()