diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc index a6a75a7e87..301f6efd4a 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc @@ -160,3 +160,15 @@ class SkipOverPHP84FinalProperties { final MyType|FALSE $propA; private static final NULL|MyClass $propB; } + +// PHP 8.4 asymmetric visibility +class WithAsym { + + private(set) NULL|TRUE $asym1 = TRUE; + + public private(set) ?bool $asym2 = FALSE; + + protected(set) FALSE|string|null $asym3 = NULL; + + public protected(set) Type|NULL|bool $asym4 = TRUE; +} diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc.fixed index 2cc52294cd..5dcd342dc9 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.1.inc.fixed @@ -160,3 +160,15 @@ class SkipOverPHP84FinalProperties { final MyType|FALSE $propA; private static final NULL|MyClass $propB; } + +// PHP 8.4 asymmetric visibility +class WithAsym { + + private(set) NULL|TRUE $asym1 = true; + + public private(set) ?bool $asym2 = false; + + protected(set) FALSE|string|null $asym3 = null; + + public protected(set) Type|NULL|bool $asym4 = true; +} diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php index a2725864fc..e4a6b80f79 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php @@ -66,6 +66,10 @@ public function getErrorList($testFile='') 129 => 1, 149 => 1, 153 => 1, + 167 => 1, + 169 => 1, + 171 => 1, + 173 => 1, ]; case 'LowerCaseConstantUnitTest.js': diff --git a/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php b/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php index 8bcf0d5689..077de01e33 100644 --- a/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php +++ b/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php @@ -50,8 +50,14 @@ public function process(File $phpcsFile, $stackPtr) $ignore = Tokens::$emptyTokens; $ignore[] = T_FINAL; + $validVisibility = [ + T_PRIVATE => T_PRIVATE, + T_PUBLIC => T_PUBLIC, + T_PROTECTED => T_PROTECTED, + ]; + $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); - if (isset(Tokens::$scopeModifiers[$tokens[$prev]['code']]) === true) { + if (isset($validVisibility[$tokens[$prev]['code']]) === true) { return; } diff --git a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.1.inc similarity index 100% rename from src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc rename to src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.1.inc diff --git a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.2.inc b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.2.inc new file mode 100644 index 0000000000..0ca9fb525b --- /dev/null +++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.2.inc @@ -0,0 +1,7 @@ + */ - public function getWarningList() + public function getWarningList($testFile='') { - return [ - 4 => 1, - 12 => 1, - 21 => 1, - ]; + switch ($testFile) { + case 'ConstantVisibilityUnitTest.1.inc': + return [ + 4 => 1, + 12 => 1, + 21 => 1, + ]; + + case 'ConstantVisibilityUnitTest.2.inc': + return [ + 6 => 1, + ]; + + default: + return []; + } }//end getWarningList() diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc index 4db25459cc..6aa9ffb799 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc @@ -96,3 +96,17 @@ class FinalProperties { public FINAL ?int $wrongOrder1; static protected final ?string $wrongOrder2; } + +class AsymmetricVisibility { + private(set) int $foo, + $bar, + $var = 5; + + public private(set) readonly ?string $spaces; + + protected(set) array $unfixed; + + protected(set) public int $wrongOrder1; + + private(set) protected ?string $wrongOrder2; +} diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed index fd5d9fa59e..efb6a0da00 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed @@ -93,3 +93,17 @@ class FinalProperties { FINAL public ?int $wrongOrder1; final protected static ?string $wrongOrder2; } + +class AsymmetricVisibility { + private(set) int $foo, + $bar, + $var = 5; + + public private(set) readonly ?string $spaces; + + protected(set) array $unfixed; + + protected(set) public int $wrongOrder1; + + private(set) protected ?string $wrongOrder2; +} diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php index 6310098525..70abcec928 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php @@ -31,36 +31,41 @@ final class PropertyDeclarationUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 7 => 1, - 9 => 2, - 10 => 1, - 11 => 1, - 17 => 1, - 18 => 1, - 23 => 1, - 38 => 1, - 41 => 1, - 42 => 1, - 50 => 2, - 51 => 1, - 55 => 1, - 56 => 1, - 61 => 1, - 62 => 1, - 68 => 1, - 69 => 1, - 71 => 1, - 72 => 1, - 76 => 1, - 80 => 1, - 82 => 1, - 84 => 1, - 86 => 1, - 90 => 1, - 94 => 1, - 95 => 1, - 96 => 1, - 97 => 2, + 7 => 1, + 9 => 2, + 10 => 1, + 11 => 1, + 17 => 1, + 18 => 1, + 23 => 1, + 38 => 1, + 41 => 1, + 42 => 1, + 50 => 2, + 51 => 1, + 55 => 1, + 56 => 1, + 61 => 1, + 62 => 1, + 68 => 1, + 69 => 1, + 71 => 1, + 72 => 1, + 76 => 1, + 80 => 1, + 82 => 1, + 84 => 1, + 86 => 1, + 90 => 1, + 94 => 1, + 95 => 1, + 96 => 1, + 97 => 2, + 101 => 2, + 105 => 1, + 107 => 1, + 109 => 1, + 111 => 1, ]; }//end getErrorList() diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 73a0c4b4a8..b67eafef97 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -403,8 +403,11 @@ class PHP extends Tokenizer T_PLUS_EQUAL => 2, T_PRINT => 5, T_PRIVATE => 7, + T_PRIVATE_SET => 12, T_PUBLIC => 6, + T_PUBLIC_SET => 11, T_PROTECTED => 9, + T_PROTECTED_SET => 14, T_READONLY => 8, T_REQUIRE => 7, T_REQUIRE_ONCE => 12, @@ -1265,6 +1268,49 @@ protected function tokenize($string) } }//end if + /* + Asymmetric visibility for PHP < 8.4 + */ + + if ($tokenIsArray === true + && in_array($token[0], [T_PUBLIC, T_PROTECTED, T_PRIVATE], true) === true + && ($stackPtr + 3) < $numTokens + && $tokens[($stackPtr + 1)] === '(' + && is_array($tokens[($stackPtr + 2)]) === true + && $tokens[($stackPtr + 2)][0] === T_STRING + && strtolower($tokens[($stackPtr + 2)][1]) === 'set' + && $tokens[($stackPtr + 3)] === ')' + ) { + $newToken = []; + if ($token[0] === T_PUBLIC) { + $oldCode = 'T_PUBLIC'; + $newToken['code'] = T_PUBLIC_SET; + $newToken['type'] = 'T_PUBLIC_SET'; + } else if ($token[0] === T_PROTECTED) { + $oldCode = 'T_PROTECTED'; + $newToken['code'] = T_PROTECTED_SET; + $newToken['type'] = 'T_PROTECTED_SET'; + } else { + $oldCode = 'T_PRIVATE'; + $newToken['code'] = T_PRIVATE_SET; + $newToken['type'] = 'T_PRIVATE_SET'; + } + + $newToken['content'] = $token[1].'('.$tokens[($stackPtr + 2)][1].')'; + $finalTokens[$newStackPtr] = $newToken; + $newStackPtr++; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $newCode = $newToken['type']; + echo "\t\t* tokens from $stackPtr changed from $oldCode to $newCode".PHP_EOL; + } + + // We processed an extra 3 tokens, for `(`, `set`, and `)`. + $stackPtr += 3; + + continue; + }//end if + /* As of PHP 8.0 fully qualified, partially qualified and namespace relative identifier names are tokenized differently. @@ -2189,6 +2235,7 @@ protected function tokenize($string) if ($tokenType === T_FUNCTION || $tokenType === T_FN || isset(Tokens::$methodPrefixes[$tokenType]) === true + || isset(Tokens::$scopeModifiers[$tokenType]) === true || $tokenType === T_VAR || $tokenType === T_READONLY ) { diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php index 73cf02ddd7..db9a2735b6 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -180,6 +180,19 @@ define('T_ENUM', 'PHPCS_T_ENUM'); } +// Some PHP 8.4 tokens, replicated for lower versions. +if (defined('T_PUBLIC_SET') === false) { + define('T_PUBLIC_SET', 'PHPCS_T_PUBLIC_SET'); +} + +if (defined('T_PROTECTED_SET') === false) { + define('T_PROTECTED_SET', 'PHPCS_T_PROTECTED_SET'); +} + +if (defined('T_PRIVATE_SET') === false) { + define('T_PRIVATE_SET', 'PHPCS_T_PRIVATE_SET'); +} + // Tokens used for parsing doc blocks. define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR'); define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE'); @@ -463,9 +476,12 @@ final class Tokens * @var array */ public static $scopeModifiers = [ - T_PRIVATE => T_PRIVATE, - T_PUBLIC => T_PUBLIC, - T_PROTECTED => T_PROTECTED, + T_PRIVATE => T_PRIVATE, + T_PUBLIC => T_PUBLIC, + T_PROTECTED => T_PROTECTED, + T_PUBLIC_SET => T_PUBLIC_SET, + T_PROTECTED_SET => T_PROTECTED_SET, + T_PRIVATE_SET => T_PRIVATE_SET, ]; /** diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc new file mode 100644 index 0000000000..aaf050ad53 --- /dev/null +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc @@ -0,0 +1,60 @@ + + * @copyright 2025 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP; + +use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase; + +/** + * Tests the support of PHP 8.4 asymmetric visibility. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + */ +final class BackfillAsymmetricVisibilityTest extends AbstractTokenizerTestCase +{ + + + /** + * Test that the asymmetric visibility keywords are tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testType The expected token type + * @param string $testContent The token content to look for + * + * @dataProvider dataAsymmetricVisibility + * + * @return void + */ + public function testAsymmetricVisibility($testMarker, $testType, $testContent) + { + $tokens = $this->phpcsFile->getTokens(); + $target = $this->getTargetToken( + $testMarker, + [ + T_PUBLIC_SET, + T_PROTECTED_SET, + T_PRIVATE_SET, + ] + ); + $tokenArray = $tokens[$target]; + + $this->assertSame( + $testType, + $tokenArray['type'], + 'Token tokenized as '.$tokenArray['type'].' (type)' + ); + $this->assertSame( + constant($testType), + $tokenArray['code'], + 'Token tokenized as '.$tokenArray['type'].' (code)' + ); + $this->assertSame( + $testContent, + $tokenArray['content'], + 'Token tokenized as '.$tokenArray['type'].' (content)' + ); + + }//end testAsymmetricVisibility() + + + /** + * Data provider. + * + * @see testAsymmetricVisibility() + * + * @return array> + */ + public static function dataAsymmetricVisibility() + { + return [ + // Normal property declarations. + 'property, public set, no read visibility, lowercase' => [ + 'testMarker' => '/* testPublicSetProperty */', + 'testType' => 'T_PUBLIC_SET', + 'testContent' => 'public(set)', + ], + 'property, public set, no read visibility, uppercase' => [ + 'testMarker' => '/* testPublicSetPropertyUC */', + 'testType' => 'T_PUBLIC_SET', + 'testContent' => 'PUBLIC(SET)', + ], + 'property, public set, has read visibility, lowercase' => [ + 'testMarker' => '/* testPublicPublicSetProperty */', + 'testType' => 'T_PUBLIC_SET', + 'testContent' => 'public(set)', + ], + 'property, public set, has read visibility, uppercase' => [ + 'testMarker' => '/* testPublicPublicSetPropertyUC */', + 'testType' => 'T_PUBLIC_SET', + 'testContent' => 'PUBLIC(SET)', + ], + 'property, protected set, no read visibility, lowercase' => [ + 'testMarker' => '/* testProtectedSetProperty */', + 'testType' => 'T_PROTECTED_SET', + 'testContent' => 'protected(set)', + ], + 'property, protected set, no read visibility, uppercase' => [ + 'testMarker' => '/* testProtectedSetPropertyUC */', + 'testType' => 'T_PROTECTED_SET', + 'testContent' => 'PROTECTED(SET)', + ], + 'property, protected set, has read visibility, lowercase' => [ + 'testMarker' => '/* testPublicProtectedSetProperty */', + 'testType' => 'T_PROTECTED_SET', + 'testContent' => 'protected(set)', + ], + 'property, protected set, has read visibility, uppercase' => [ + 'testMarker' => '/* testPublicProtectedSetPropertyUC */', + 'testType' => 'T_PROTECTED_SET', + 'testContent' => 'PROTECTED(SET)', + ], + 'property, private set, no read visibility, lowercase' => [ + 'testMarker' => '/* testPrivateSetProperty */', + 'testType' => 'T_PRIVATE_SET', + 'testContent' => 'private(set)', + ], + 'property, private set, no read visibility, uppercase' => [ + 'testMarker' => '/* testPrivateSetPropertyUC */', + 'testType' => 'T_PRIVATE_SET', + 'testContent' => 'PRIVATE(SET)', + ], + 'property, private set, has read visibility, lowercase' => [ + 'testMarker' => '/* testPublicPrivateSetProperty */', + 'testType' => 'T_PRIVATE_SET', + 'testContent' => 'private(set)', + ], + 'property, private set, has read visibility, uppercase' => [ + 'testMarker' => '/* testPublicPrivateSetPropertyUC */', + 'testType' => 'T_PRIVATE_SET', + 'testContent' => 'PRIVATE(SET)', + ], + + // Constructor property promotion. + 'promotion, public set, no read visibility, lowercase' => [ + 'testMarker' => '/* testPublicSetCPP */', + 'testType' => 'T_PUBLIC_SET', + 'testContent' => 'public(set)', + ], + 'promotion, public set, no read visibility, uppercase' => [ + 'testMarker' => '/* testPublicSetCPPUC */', + 'testType' => 'T_PUBLIC_SET', + 'testContent' => 'PUBLIC(SET)', + ], + 'promotion, public set, has read visibility, lowercase' => [ + 'testMarker' => '/* testPublicPublicSetCPP */', + 'testType' => 'T_PUBLIC_SET', + 'testContent' => 'public(set)', + ], + 'promotion, public set, has read visibility, uppercase' => [ + 'testMarker' => '/* testPublicPublicSetCPPUC */', + 'testType' => 'T_PUBLIC_SET', + 'testContent' => 'PUBLIC(SET)', + ], + 'promotion, protected set, no read visibility, lowercase' => [ + 'testMarker' => '/* testProtectedSetCPP */', + 'testType' => 'T_PROTECTED_SET', + 'testContent' => 'protected(set)', + ], + 'promotion, protected set, no read visibility, uppercase' => [ + 'testMarker' => '/* testProtectedSetCPPUC */', + 'testType' => 'T_PROTECTED_SET', + 'testContent' => 'PROTECTED(SET)', + ], + 'promotion, protected set, has read visibility, lowercase' => [ + 'testMarker' => '/* testPublicProtectedSetCPP */', + 'testType' => 'T_PROTECTED_SET', + 'testContent' => 'protected(set)', + ], + 'promotion, protected set, has read visibility, uppercase' => [ + 'testMarker' => '/* testPublicProtectedSetCPPUC */', + 'testType' => 'T_PROTECTED_SET', + 'testContent' => 'PROTECTED(SET)', + ], + 'promotion, private set, no read visibility, lowercase' => [ + 'testMarker' => '/* testPrivateSetCPP */', + 'testType' => 'T_PRIVATE_SET', + 'testContent' => 'private(set)', + ], + 'promotion, private set, no read visibility, uppercase' => [ + 'testMarker' => '/* testPrivateSetCPPUC */', + 'testType' => 'T_PRIVATE_SET', + 'testContent' => 'PRIVATE(SET)', + ], + 'promotion, private set, has read visibility, lowercase' => [ + 'testMarker' => '/* testPublicPrivateSetCPP */', + 'testType' => 'T_PRIVATE_SET', + 'testContent' => 'private(set)', + ], + 'promotion, private set, has read visibility, uppercase' => [ + 'testMarker' => '/* testPublicPrivateSetCPPUC */', + 'testType' => 'T_PRIVATE_SET', + 'testContent' => 'PRIVATE(SET)', + ], + ]; + + }//end dataAsymmetricVisibility() + + + /** + * Test that things that are not asymmetric visibility keywords are not + * tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testType The expected token type + * @param string $testContent The token content to look for + * + * @dataProvider dataNotAsymmetricVisibility + * + * @return void + */ + public function testNotAsymmetricVisibility($testMarker, $testType, $testContent) + { + $tokens = $this->phpcsFile->getTokens(); + $target = $this->getTargetToken( + $testMarker, + [constant($testType)], + $testContent + ); + $tokenArray = $tokens[$target]; + + $this->assertSame( + $testType, + $tokenArray['type'], + 'Token tokenized as '.$tokenArray['type'].' (type)' + ); + $this->assertSame( + constant($testType), + $tokenArray['code'], + 'Token tokenized as '.$tokenArray['type'].' (code)' + ); + + }//end testNotAsymmetricVisibility() + + + /** + * Data provider. + * + * @see testNotAsymmetricVisibility() + * + * @return array> + */ + public static function dataNotAsymmetricVisibility() + { + return [ + 'property, invalid case 1' => [ + 'testMarker' => '/* testInvalidUnsetProperty */', + 'testType' => 'T_PUBLIC', + 'testContent' => 'public', + ], + 'property, invalid case 2' => [ + 'testMarker' => '/* testInvalidSpaceProperty */', + 'testType' => 'T_PUBLIC', + 'testContent' => 'public', + ], + 'property, invalid case 3' => [ + 'testMarker' => '/* testInvalidCommentProperty */', + 'testType' => 'T_PROTECTED', + 'testContent' => 'protected', + ], + 'property, invalid case 4' => [ + 'testMarker' => '/* testInvalidGetProperty */', + 'testType' => 'T_PRIVATE', + 'testContent' => 'private', + ], + 'property, invalid case 5' => [ + 'testMarker' => '/* testInvalidNoParenProperty */', + 'testType' => 'T_PRIVATE', + 'testContent' => 'private', + ], + + // Constructor property promotion. + 'promotion, invalid case 1' => [ + 'testMarker' => '/* testInvalidUnsetCPP */', + 'testType' => 'T_PUBLIC', + 'testContent' => 'public', + ], + 'promotion, invalid case 2' => [ + 'testMarker' => '/* testInvalidSpaceCPP */', + 'testType' => 'T_PUBLIC', + 'testContent' => 'public', + ], + 'promotion, invalid case 3' => [ + 'testMarker' => '/* testInvalidCommentCPP */', + 'testType' => 'T_PROTECTED', + 'testContent' => 'protected', + ], + 'promotion, invalid case 4' => [ + 'testMarker' => '/* testInvalidGetCPP */', + 'testType' => 'T_PRIVATE', + 'testContent' => 'private', + ], + 'promotion, invalid case 5' => [ + 'testMarker' => '/* testInvalidNoParenCPP */', + 'testType' => 'T_PRIVATE', + 'testContent' => 'private', + ], + + // Context sensitivitiy. + 'protected as function name' => [ + 'testMarker' => '/* testProtectedFunctionName */', + 'testType' => 'T_STRING', + 'testContent' => 'protected', + ], + 'public as function name' => [ + 'testMarker' => '/* testPublicFunctionName */', + 'testType' => 'T_STRING', + 'testContent' => 'public', + ], + 'set as parameter type' => [ + 'testMarker' => '/* testSetParameterType */', + 'testType' => 'T_STRING', + 'testContent' => 'Set', + ], + + // Live coding. + 'live coding' => [ + 'testMarker' => '/* testLiveCoding */', + 'testType' => 'T_PRIVATE', + 'testContent' => 'private', + ], + ]; + + }//end dataNotAsymmetricVisibility() + + +}//end class diff --git a/tests/Core/Tokenizers/PHP/BitwiseOrTest.inc b/tests/Core/Tokenizers/PHP/BitwiseOrTest.inc index c2c4508e43..822b4413c4 100644 --- a/tests/Core/Tokenizers/PHP/BitwiseOrTest.inc +++ b/tests/Core/Tokenizers/PHP/BitwiseOrTest.inc @@ -84,6 +84,18 @@ class TypeUnion /* testTypeUnionWithPHP84FinalKeywordAndFQN */ final \MyClass|false $finalKeywordC; + /* testTypeUnionPropertyPrivateSet */ + private(set) Foo|Bar $asym1; + + /* testTypeUnionPropertyPublicPrivateSet */ + public private(set) Foo|Bar $asym2; + + /* testTypeUnionPropertyProtected */ + protected(set) Foo|Bar $asym3; + + /* testTypeUnionPropertyPublicProtected */ + public protected(set) Foo|Bar $asym4; + public function paramTypes( /* testTypeUnionParam1 */ int|float $paramA /* testBitwiseOrParamDefaultValue */ = CONSTANT_A | CONSTANT_B, diff --git a/tests/Core/Tokenizers/PHP/BitwiseOrTest.php b/tests/Core/Tokenizers/PHP/BitwiseOrTest.php index ee1ff84f2a..2385ae1e72 100644 --- a/tests/Core/Tokenizers/PHP/BitwiseOrTest.php +++ b/tests/Core/Tokenizers/PHP/BitwiseOrTest.php @@ -128,6 +128,10 @@ public static function dataTypeUnion() 'type for final property, no visibility' => ['/* testTypeUnionWithPHP84FinalKeyword */'], 'type for final property, reversed modifier order' => ['/* testTypeUnionWithPHP84FinalKeywordFirst */'], 'type for final property, no visibility, FQN type' => ['/* testTypeUnionWithPHP84FinalKeywordAndFQN */'], + 'type for private(set) property' => ['/* testTypeUnionPropertyPrivateSet */'], + 'type for public private(set) property' => ['/* testTypeUnionPropertyPublicPrivateSet */'], + 'type for protected(set) property' => ['/* testTypeUnionPropertyProtected */'], + 'type for public protected(set) property' => ['/* testTypeUnionPropertyPublicProtected */'], 'type for method parameter' => ['/* testTypeUnionParam1 */'], 'type for method parameter, first in multi-union' => ['/* testTypeUnionParam2 */'], 'type for method parameter, last in multi-union' => ['/* testTypeUnionParam3 */'], diff --git a/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.inc index 2825f26e52..e019dfe8a3 100644 --- a/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.inc +++ b/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.inc @@ -102,6 +102,10 @@ namespace /* testNamespaceNameIsString1 */ my\ /* testNamespaceNameIsString2 */ /* testProtectedIsKeyword */ protected $protected; /* testPublicIsKeyword */ public $public; + /* testPrivateSetIsKeyword */ private(set) mixed $privateSet; + /* testProtectedSetIsKeyword */ protected(set) mixed $protectedSet; + /* testPublicSetIsKeyword */ public(set) mixed $publicSet; + /* testVarIsKeyword */ var $var; /* testStaticIsKeyword */ static $static; diff --git a/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php index a8d746ae9f..09ff9050f1 100644 --- a/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php @@ -155,8 +155,16 @@ public static function dataStrings() */ public function testKeywords($testMarker, $expectedTokenType) { + $tokenTargets = Tokens::$contextSensitiveKeywords; + $tokenTargets[] = T_STRING; + $tokenTargets[] = T_ANON_CLASS; + $tokenTargets[] = T_MATCH_DEFAULT; + $tokenTargets[] = T_PRIVATE_SET; + $tokenTargets[] = T_PROTECTED_SET; + $tokenTargets[] = T_PUBLIC_SET; + $tokens = $this->phpcsFile->getTokens(); - $target = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_ANON_CLASS, T_MATCH_DEFAULT, T_STRING])); + $target = $this->getTargetToken($testMarker, $tokenTargets); $tokenArray = $tokens[$target]; $this->assertSame( @@ -244,6 +252,18 @@ public static function dataKeywords() 'testMarker' => '/* testPublicIsKeyword */', 'expectedTokenType' => 'T_PUBLIC', ], + 'private(set): property declaration' => [ + 'testMarker' => '/* testPrivateSetIsKeyword */', + 'expectedTokenType' => 'T_PRIVATE_SET', + ], + 'protected(set): property declaration' => [ + 'testMarker' => '/* testProtectedSetIsKeyword */', + 'expectedTokenType' => 'T_PROTECTED_SET', + ], + 'public(set): property declaration' => [ + 'testMarker' => '/* testPublicSetIsKeyword */', + 'expectedTokenType' => 'T_PUBLIC_SET', + ], 'var: property declaration' => [ 'testMarker' => '/* testVarIsKeyword */', 'expectedTokenType' => 'T_VAR', diff --git a/tests/Core/Tokenizers/PHP/DNFTypesTest.inc b/tests/Core/Tokenizers/PHP/DNFTypesTest.inc index c1a38c79e5..ae9dc94488 100644 --- a/tests/Core/Tokenizers/PHP/DNFTypesTest.inc +++ b/tests/Core/Tokenizers/PHP/DNFTypesTest.inc @@ -181,6 +181,18 @@ abstract class DNFTypes { /* testDNFTypeWithPHP84FinalKeywordAndStatic */ final static (\className&\InterfaceName)|false $finalKeywordB; + /* testDNFTypePropertyWithPrivateSet */ + private(set) (A&B&C)|true $asym1; + + /* testDNFTypePropertyWithPublicPrivateSet */ + public private(set) (A&B&C)|true $asym2; + + /* testDNFTypePropertyWithProtectedSet */ + protected(set) (A&B&C)|true $asym3; + + /* testDNFTypePropertyWithPublicProtectedSet */ + public protected(set) (A&B&C)|true $asym4; + public function paramTypes( /* testDNFTypeParam1WithAttribute */ #[MyAttribute] diff --git a/tests/Core/Tokenizers/PHP/DNFTypesTest.php b/tests/Core/Tokenizers/PHP/DNFTypesTest.php index 3c9fcb80bc..98701f135f 100644 --- a/tests/Core/Tokenizers/PHP/DNFTypesTest.php +++ b/tests/Core/Tokenizers/PHP/DNFTypesTest.php @@ -450,7 +450,18 @@ public static function dataDNFTypeParentheses() 'OO property type: with final and static keyword' => [ 'testMarker' => '/* testDNFTypeWithPHP84FinalKeywordAndStatic */', ], - + 'OO property type: asymmetric visibility, private(set)' => [ + 'testMarker' => '/* testDNFTypePropertyWithPrivateSet */', + ], + 'OO property type: asymmetric vis, public private(set)' => [ + 'testMarker' => '/* testDNFTypePropertyWithPublicPrivateSet */', + ], + 'OO property type: asymmetric visibility, protected(set)' => [ + 'testMarker' => '/* testDNFTypePropertyWithProtectedSet */', + ], + 'OO property type: asymmetric vis, public protected(set)' => [ + 'testMarker' => '/* testDNFTypePropertyWithPublicProtectedSet */', + ], 'OO method param type: first param' => [ 'testMarker' => '/* testDNFTypeParam1WithAttribute */', ], diff --git a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc index 4c018af631..56ad3856c3 100644 --- a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc +++ b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc @@ -4,6 +4,12 @@ class Nullable { /* testNullableReadonlyOnly */ readonly ?int $prop; + + /* testNullablePrivateSet */ + private(set) ?int $prop2; + + /* testNullablePublicProtectedSet */ + public protected(set) ?int $prop3; } class InlineThen diff --git a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php index fc7ed4b105..88920b5601 100644 --- a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php +++ b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php @@ -50,7 +50,9 @@ public function testNullable($testMarker) public static function dataNullable() { return [ - 'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'], + 'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'], + 'property declaration, private set' => ['/* testNullablePrivateSet */'], + 'property declaration, public and protected set' => ['/* testNullablePublicProtectedSet */'], ]; }//end dataNullable() diff --git a/tests/Core/Tokenizers/PHP/TypeIntersectionTest.inc b/tests/Core/Tokenizers/PHP/TypeIntersectionTest.inc index fadc0df85a..54b3c06e19 100644 --- a/tests/Core/Tokenizers/PHP/TypeIntersectionTest.inc +++ b/tests/Core/Tokenizers/PHP/TypeIntersectionTest.inc @@ -69,6 +69,18 @@ class TypeIntersection /* testTypeIntersectionWithPHP84FinalKeywordFirst */ final private \className&InterfaceName $finalKeywordB; + /* testTypeIntersectionPropertyWithPrivateSet */ + private(set) Foo&Bar $asym1; + + /* testTypeIntersectionPropertyWithPublicPrivateSet */ + public private(set) Foo&Bar $asym2; + + /* testTypeIntersectionPropertyWithProtectedSet */ + protected(set) Foo&Bar $asym3; + + /* testTypeIntersectionPropertyWithPublicProtectedSet */ + public protected(set) Foo&Bar $asym4; + public function paramTypes( /* testTypeIntersectionParam1 */ Foo&Bar $paramA /* testBitwiseAndParamDefaultValue */ = CONSTANT_A & CONSTANT_B, diff --git a/tests/Core/Tokenizers/PHP/TypeIntersectionTest.php b/tests/Core/Tokenizers/PHP/TypeIntersectionTest.php index b191d7ebd4..3a4eb0070c 100644 --- a/tests/Core/Tokenizers/PHP/TypeIntersectionTest.php +++ b/tests/Core/Tokenizers/PHP/TypeIntersectionTest.php @@ -126,6 +126,10 @@ public static function dataTypeIntersection() 'type for static readonly property' => ['/* testTypeIntersectionPropertyWithStaticKeyword */'], 'type for final property' => ['/* testTypeIntersectionWithPHP84FinalKeyword */'], 'type for final property reversed modifier order' => ['/* testTypeIntersectionWithPHP84FinalKeywordFirst */'], + 'type for asymmetric visibility (private(set)) property' => ['/* testTypeIntersectionPropertyWithPrivateSet */'], + 'type for asymmetric visibility (public private(set)) prop' => ['/* testTypeIntersectionPropertyWithPublicPrivateSet */'], + 'type for asymmetric visibility (protected(set)) property' => ['/* testTypeIntersectionPropertyWithProtectedSet */'], + 'type for asymmetric visibility (public protected(set)) prop' => ['/* testTypeIntersectionPropertyWithPublicProtectedSet */'], 'type for method parameter' => ['/* testTypeIntersectionParam1 */'], 'type for method parameter, first in multi-intersect' => ['/* testTypeIntersectionParam2 */'], 'type for method parameter, last in multi-intersect' => ['/* testTypeIntersectionParam3 */'], diff --git a/tests/Core/Util/Tokens/TokenNameTest.php b/tests/Core/Util/Tokens/TokenNameTest.php index 28c6c613c2..43c6d695ba 100644 --- a/tests/Core/Util/Tokens/TokenNameTest.php +++ b/tests/Core/Util/Tokens/TokenNameTest.php @@ -176,6 +176,19 @@ public static function dataPolyfilledPHPNativeTokens() 'tokenCode' => T_ENUM, 'expected' => 'T_ENUM', ], + + 'PHP 8.4 native token, polyfilled: T_PUBLIC_SET' => [ + 'tokenCode' => T_PUBLIC_SET, + 'expected' => 'T_PUBLIC_SET', + ], + 'PHP 8.4 native token, polyfilled: T_PROTECTED_SET' => [ + 'tokenCode' => T_PROTECTED_SET, + 'expected' => 'T_PROTECTED_SET', + ], + 'PHP 8.4 native token, polyfilled: T_PRIVATE_SET' => [ + 'tokenCode' => T_PRIVATE_SET, + 'expected' => 'T_PRIVATE_SET', + ], ]; }//end dataPolyfilledPHPNativeTokens()