From 986d4efabdf8ffe6d0431ca23d695a06d58fb6a3 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 11 Mar 2025 15:55:32 -0700 Subject: [PATCH 01/20] PHP 8.4 | Add tokenization of asymmetric visibility Part of #851 --- src/Tokenizers/PHP.php | 45 ++++ src/Util/Tokens.php | 165 ++++++++------- .../PHP/BackfillAsymmetricVisibilityTest.inc | 37 ++++ .../PHP/BackfillAsymmetricVisibilityTest.php | 198 ++++++++++++++++++ .../PHP/ContextSensitiveKeywordsTest.inc | 4 + .../PHP/ContextSensitiveKeywordsTest.php | 12 ++ 6 files changed, 388 insertions(+), 73 deletions(-) create mode 100644 tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc create mode 100644 tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 73a0c4b4a8..d711ab7908 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,48 @@ 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)] === '(' + && $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. diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php index 73cf02ddd7..ee01f00603 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, ]; /** @@ -678,76 +694,79 @@ final class Tokens * https://wiki.php.net/rfc/context_sensitive_lexer */ public static $contextSensitiveKeywords = [ - T_ABSTRACT => T_ABSTRACT, - T_ARRAY => T_ARRAY, - T_AS => T_AS, - T_BREAK => T_BREAK, - T_CALLABLE => T_CALLABLE, - T_CASE => T_CASE, - T_CATCH => T_CATCH, - T_CLASS => T_CLASS, - T_CLONE => T_CLONE, - T_CONST => T_CONST, - T_CONTINUE => T_CONTINUE, - T_DECLARE => T_DECLARE, - T_DEFAULT => T_DEFAULT, - T_DO => T_DO, - T_ECHO => T_ECHO, - T_ELSE => T_ELSE, - T_ELSEIF => T_ELSEIF, - T_EMPTY => T_EMPTY, - T_ENDDECLARE => T_ENDDECLARE, - T_ENDFOR => T_ENDFOR, - T_ENDFOREACH => T_ENDFOREACH, - T_ENDIF => T_ENDIF, - T_ENDSWITCH => T_ENDSWITCH, - T_ENDWHILE => T_ENDWHILE, - T_ENUM => T_ENUM, - T_EVAL => T_EVAL, - T_EXIT => T_EXIT, - T_EXTENDS => T_EXTENDS, - T_FINAL => T_FINAL, - T_FINALLY => T_FINALLY, - T_FN => T_FN, - T_FOR => T_FOR, - T_FOREACH => T_FOREACH, - T_FUNCTION => T_FUNCTION, - T_GLOBAL => T_GLOBAL, - T_GOTO => T_GOTO, - T_IF => T_IF, - T_IMPLEMENTS => T_IMPLEMENTS, - T_INCLUDE => T_INCLUDE, - T_INCLUDE_ONCE => T_INCLUDE_ONCE, - T_INSTANCEOF => T_INSTANCEOF, - T_INSTEADOF => T_INSTEADOF, - T_INTERFACE => T_INTERFACE, - T_ISSET => T_ISSET, - T_LIST => T_LIST, - T_LOGICAL_AND => T_LOGICAL_AND, - T_LOGICAL_OR => T_LOGICAL_OR, - T_LOGICAL_XOR => T_LOGICAL_XOR, - T_MATCH => T_MATCH, - T_NAMESPACE => T_NAMESPACE, - T_NEW => T_NEW, - T_PRINT => T_PRINT, - T_PRIVATE => T_PRIVATE, - T_PROTECTED => T_PROTECTED, - T_PUBLIC => T_PUBLIC, - T_READONLY => T_READONLY, - T_REQUIRE => T_REQUIRE, - T_REQUIRE_ONCE => T_REQUIRE_ONCE, - T_RETURN => T_RETURN, - T_STATIC => T_STATIC, - T_SWITCH => T_SWITCH, - T_THROW => T_THROW, - T_TRAIT => T_TRAIT, - T_TRY => T_TRY, - T_UNSET => T_UNSET, - T_USE => T_USE, - T_VAR => T_VAR, - T_WHILE => T_WHILE, - T_YIELD => T_YIELD, - T_YIELD_FROM => T_YIELD_FROM, + T_ABSTRACT => T_ABSTRACT, + T_ARRAY => T_ARRAY, + T_AS => T_AS, + T_BREAK => T_BREAK, + T_CALLABLE => T_CALLABLE, + T_CASE => T_CASE, + T_CATCH => T_CATCH, + T_CLASS => T_CLASS, + T_CLONE => T_CLONE, + T_CONST => T_CONST, + T_CONTINUE => T_CONTINUE, + T_DECLARE => T_DECLARE, + T_DEFAULT => T_DEFAULT, + T_DO => T_DO, + T_ECHO => T_ECHO, + T_ELSE => T_ELSE, + T_ELSEIF => T_ELSEIF, + T_EMPTY => T_EMPTY, + T_ENDDECLARE => T_ENDDECLARE, + T_ENDFOR => T_ENDFOR, + T_ENDFOREACH => T_ENDFOREACH, + T_ENDIF => T_ENDIF, + T_ENDSWITCH => T_ENDSWITCH, + T_ENDWHILE => T_ENDWHILE, + T_ENUM => T_ENUM, + T_EVAL => T_EVAL, + T_EXIT => T_EXIT, + T_EXTENDS => T_EXTENDS, + T_FINAL => T_FINAL, + T_FINALLY => T_FINALLY, + T_FN => T_FN, + T_FOR => T_FOR, + T_FOREACH => T_FOREACH, + T_FUNCTION => T_FUNCTION, + T_GLOBAL => T_GLOBAL, + T_GOTO => T_GOTO, + T_IF => T_IF, + T_IMPLEMENTS => T_IMPLEMENTS, + T_INCLUDE => T_INCLUDE, + T_INCLUDE_ONCE => T_INCLUDE_ONCE, + T_INSTANCEOF => T_INSTANCEOF, + T_INSTEADOF => T_INSTEADOF, + T_INTERFACE => T_INTERFACE, + T_ISSET => T_ISSET, + T_LIST => T_LIST, + T_LOGICAL_AND => T_LOGICAL_AND, + T_LOGICAL_OR => T_LOGICAL_OR, + T_LOGICAL_XOR => T_LOGICAL_XOR, + T_MATCH => T_MATCH, + T_NAMESPACE => T_NAMESPACE, + T_NEW => T_NEW, + T_PRINT => T_PRINT, + T_PRIVATE => T_PRIVATE, + T_PRIVATE_SET => T_PRIVATE_SET, + T_PROTECTED => T_PROTECTED, + T_PROTECTED_SET => T_PROTECTED_SET, + T_PUBLIC => T_PUBLIC, + T_PUBLIC_SET => T_PUBLIC_SET, + T_READONLY => T_READONLY, + T_REQUIRE => T_REQUIRE, + T_REQUIRE_ONCE => T_REQUIRE_ONCE, + T_RETURN => T_RETURN, + T_STATIC => T_STATIC, + T_SWITCH => T_SWITCH, + T_THROW => T_THROW, + T_TRAIT => T_TRAIT, + T_TRY => T_TRY, + T_UNSET => T_UNSET, + T_USE => T_USE, + T_VAR => T_VAR, + T_WHILE => T_WHILE, + T_YIELD => T_YIELD, + T_YIELD_FROM => T_YIELD_FROM, ]; diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc new file mode 100644 index 0000000000..7a8e1e44be --- /dev/null +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc @@ -0,0 +1,37 @@ + + * @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; + +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 + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testAsymmetricVisibility( + string $testMarker, + string $testType, + string $testContent + ) { + $tokens = $this->phpcsFile->getTokens(); + $target = $this->getTargetToken( + $testMarker, + [ + T_PUBLIC_SET, + T_PROTECTED_SET, + T_PRIVATE_SET, + ], + $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 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() + + +}//end class 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..2f6ba9b8da 100644 --- a/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php @@ -236,14 +236,26 @@ public static function dataKeywords() 'testMarker' => '/* testPrivateIsKeyword */', 'expectedTokenType' => 'T_PRIVATE', ], + 'private(set): property declaration' => [ + 'testMarker' => '/* testPrivateSetIsKeyword */', + 'expectedTokenType' => 'T_PRIVATE_SET', + ], 'protected: property declaration' => [ 'testMarker' => '/* testProtectedIsKeyword */', 'expectedTokenType' => 'T_PROTECTED', ], + 'protected(set): property declaration' => [ + 'testMarker' => '/* testProtectedSetIsKeyword */', + 'expectedTokenType' => 'T_PROTECTED_SET', + ], 'public: property declaration' => [ 'testMarker' => '/* testPublicIsKeyword */', 'expectedTokenType' => 'T_PUBLIC', ], + 'public(set): property declaration' => [ + 'testMarker' => '/* testPublicSetIsKeyword */', + 'expectedTokenType' => 'T_PUBLIC_SET', + ], 'var: property declaration' => [ 'testMarker' => '/* testVarIsKeyword */', 'expectedTokenType' => 'T_VAR', From b3e48169e941abf33620199262effdbc212ba8d2 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 11 Mar 2025 16:10:11 -0700 Subject: [PATCH 02/20] Drop typehints Tests need to pass on PHP 5 where `string` wasn't reserved yet --- .../Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php index 0980ca5512..fa3b6ba1fe 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php @@ -26,11 +26,8 @@ final class BackfillAsymmetricVisibilityTest extends AbstractTokenizerTestCase * * @return void */ - public function testAsymmetricVisibility( - string $testMarker, - string $testType, - string $testContent - ) { + public function testAsymmetricVisibility($testMarker, $testType, $testContent) + { $tokens = $this->phpcsFile->getTokens(); $target = $this->getTargetToken( $testMarker, From bd3aa78351cfa540456b0aaa52e0e815231ed1b0 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 18 Mar 2025 15:48:12 -0700 Subject: [PATCH 03/20] Asymmetric visibility: expand tests, check is_array --- src/Tokenizers/PHP.php | 1 + .../PHP/BackfillAsymmetricVisibilityTest.inc | 15 +++++ .../PHP/BackfillAsymmetricVisibilityTest.php | 59 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index d711ab7908..dde6162c4e 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -1276,6 +1276,7 @@ protected function tokenize($string) && in_array($token[0], [T_PUBLIC, T_PROTECTED, T_PRIVATE], true) === true && ($stackPtr + 3) < $numTokens && $tokens[($stackPtr + 1)] === '(' + && is_array($tokens[($stackPtr + 2)]) && $tokens[($stackPtr + 2)][0] === T_STRING && strtolower($tokens[($stackPtr + 2)][1]) === 'set' && $tokens[($stackPtr + 3)] === ')' diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc index 7a8e1e44be..e188650104 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc @@ -15,6 +15,12 @@ class PropertyDemo { /* testPrivateSetPropertyUC */ PRIVATE(SET) mixed $priv2; public /* testPublicPrivateSetProperty */ private(set) mixed $priv3; public /* testPublicPrivateSetPropertyUC */ PRIVATE(SET) mixed $priv4; + + /* testInvalidUnsetProperty */ public mixed $invalid1; + /* testInvalidSpaceProperty */ public (set) mixed $invalid2; + /* testInvalidCommentProperty */ protected/* foo */(set) mixed $invalid3; + /* testInvalidGetProperty */ private(get) mixed $invalid4; + /* testInvalidNoParenProperty */ private set mixed $invalid5; } class ConstructorPromotionDemo { @@ -33,5 +39,14 @@ class ConstructorPromotionDemo { /* testPrivateSetCPPUC */ PRIVATE(SET) mixed $priv2, public /* testPublicPrivateSetCPP */ private(set) mixed $priv3, public /* testPublicPrivateSetCPPUC */ PRIVATE(SET) mixed $priv4, + + /* testInvalidUnsetCPP */ public(unset) mixed $invalid1, + /* testInvalidSpaceCPP */ public (set) mixed $invalid2, + /* testInvalidCommentCPP */ protected/* foo */(set) mixed $invalid3, + /* testInvalidGetCPP */ private(get) mixed $invalid4, + /* testInvalidNoParenCPP */ private set mixed $invalid5, ) {} } + +class LiveCodingDemo { + /* testLiveCoding */ private(set diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php index fa3b6ba1fe..e3f4386cb0 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php @@ -35,6 +35,8 @@ public function testAsymmetricVisibility($testMarker, $testType, $testContent) T_PUBLIC_SET, T_PROTECTED_SET, T_PRIVATE_SET, + // For error cases + constant($testType), ], $testContent ); @@ -125,6 +127,31 @@ public static function dataAsymmetricVisibility() 'testType' => 'T_PRIVATE_SET', 'testContent' => 'PRIVATE(SET)', ], + '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, public set, no read visibility, lowercase' => [ @@ -187,6 +214,38 @@ public static function dataAsymmetricVisibility() 'testType' => 'T_PRIVATE_SET', 'testContent' => 'PRIVATE(SET)', ], + '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', + ], + + // Live coding. + 'live coding' => [ + 'testMarker' => '/* testLiveCoding */', + 'testType' => 'T_PRIVATE', + 'testContent' => 'private', + ] ]; }//end dataAsymmetricVisibility() From baa8fc29ef8e9e563303bdd768708c9d8b1f1ac0 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 18 Mar 2025 15:51:38 -0700 Subject: [PATCH 04/20] === true --- src/Tokenizers/PHP.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index dde6162c4e..e6bc072235 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -1276,7 +1276,7 @@ protected function tokenize($string) && in_array($token[0], [T_PUBLIC, T_PROTECTED, T_PRIVATE], true) === true && ($stackPtr + 3) < $numTokens && $tokens[($stackPtr + 1)] === '(' - && is_array($tokens[($stackPtr + 2)]) + && is_array($tokens[($stackPtr + 2)]) === true && $tokens[($stackPtr + 2)][0] === T_STRING && strtolower($tokens[($stackPtr + 2)][1]) === 'set' && $tokens[($stackPtr + 3)] === ')' From c8fdc9af22b6d69eb6203f2dca12b9cfc66a1a42 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 18 Mar 2025 15:55:33 -0700 Subject: [PATCH 05/20] 2 more fixes --- .../Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php index e3f4386cb0..558ea33324 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php @@ -35,7 +35,7 @@ public function testAsymmetricVisibility($testMarker, $testType, $testContent) T_PUBLIC_SET, T_PROTECTED_SET, T_PRIVATE_SET, - // For error cases + // For error cases. constant($testType), ], $testContent @@ -245,7 +245,7 @@ public static function dataAsymmetricVisibility() 'testMarker' => '/* testLiveCoding */', 'testType' => 'T_PRIVATE', 'testContent' => 'private', - ] + ], ]; }//end dataAsymmetricVisibility() From d90084a10176ad07f401e840cd4d6c91b7de575b Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 31 Mar 2025 12:32:11 -0700 Subject: [PATCH 06/20] Expand and improve tests --- .../PHP/BackfillAsymmetricVisibilityTest.inc | 9 +- .../PHP/BackfillAsymmetricVisibilityTest.php | 131 ++++++++++++++---- tests/Core/Tokenizers/PHP/BitwiseOrTest.inc | 12 ++ tests/Core/Tokenizers/PHP/BitwiseOrTest.php | 4 + .../PHP/ContextSensitiveKeywordsTest.php | 16 +-- tests/Core/Tokenizers/PHP/DNFTypesTest.inc | 12 ++ tests/Core/Tokenizers/PHP/DNFTypesTest.php | 13 +- .../Tokenizers/PHP/TypeIntersectionTest.inc | 12 ++ .../Tokenizers/PHP/TypeIntersectionTest.php | 4 + 9 files changed, 174 insertions(+), 39 deletions(-) diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc index e188650104..161a93e853 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc @@ -16,7 +16,7 @@ class PropertyDemo { public /* testPublicPrivateSetProperty */ private(set) mixed $priv3; public /* testPublicPrivateSetPropertyUC */ PRIVATE(SET) mixed $priv4; - /* testInvalidUnsetProperty */ public mixed $invalid1; + /* testInvalidUnsetProperty */ public(unset) mixed $invalid1; /* testInvalidSpaceProperty */ public (set) mixed $invalid2; /* testInvalidCommentProperty */ protected/* foo */(set) mixed $invalid3; /* testInvalidGetProperty */ private(get) mixed $invalid4; @@ -48,5 +48,12 @@ class ConstructorPromotionDemo { ) {} } +class NonVisibilityCases { + function /* testProtectedFunctionName */ protected() {} + function /* testPublicFunctionName */ public( + /* testSetParameterType */ Set $setter + ) {} +} + class LiveCodingDemo { /* testLiveCoding */ private(set diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php index 558ea33324..6acb8ba431 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php @@ -35,10 +35,7 @@ public function testAsymmetricVisibility($testMarker, $testType, $testContent) T_PUBLIC_SET, T_PROTECTED_SET, T_PRIVATE_SET, - // For error cases. - constant($testType), - ], - $testContent + ] ); $tokenArray = $tokens[$target]; @@ -52,10 +49,52 @@ public function testAsymmetricVisibility($testMarker, $testType, $testContent) $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].' (code)' ); + $this->assertSame( + $testContent, + $tokenArray['content'], + 'Token tokenized as '.$tokenArray['type'].' (content)' + ); }//end testAsymmetricVisibility() + /** + * 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 + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @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. * @@ -127,31 +166,6 @@ public static function dataAsymmetricVisibility() 'testType' => 'T_PRIVATE_SET', 'testContent' => 'PRIVATE(SET)', ], - '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, public set, no read visibility, lowercase' => [ @@ -214,6 +228,48 @@ public static function dataAsymmetricVisibility() 'testType' => 'T_PRIVATE_SET', 'testContent' => 'PRIVATE(SET)', ], + ]; + + }//end dataAsymmetricVisibility() + + + /** + * 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', @@ -240,6 +296,23 @@ public static function dataAsymmetricVisibility() '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 */', 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.php b/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php index 2f6ba9b8da..1b6f871393 100644 --- a/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php @@ -236,22 +236,22 @@ public static function dataKeywords() 'testMarker' => '/* testPrivateIsKeyword */', 'expectedTokenType' => 'T_PRIVATE', ], - 'private(set): property declaration' => [ - 'testMarker' => '/* testPrivateSetIsKeyword */', - 'expectedTokenType' => 'T_PRIVATE_SET', - ], 'protected: property declaration' => [ 'testMarker' => '/* testProtectedIsKeyword */', 'expectedTokenType' => 'T_PROTECTED', ], - 'protected(set): property declaration' => [ - 'testMarker' => '/* testProtectedSetIsKeyword */', - 'expectedTokenType' => 'T_PROTECTED_SET', - ], 'public: property declaration' => [ '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', 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/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 */'], From 6c24745d3b9eecce273525bace5b38d5e285f475 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 31 Mar 2025 12:35:39 -0700 Subject: [PATCH 07/20] PHPCS fixes --- .../PHP/BackfillAsymmetricVisibilityTest.php | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php index 6acb8ba431..deb66fb5fa 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php @@ -243,85 +243,85 @@ public static function dataAsymmetricVisibility() public static function dataNotAsymmetricVisibility() { return [ - 'property, invalid case 1' => [ + 'property, invalid case 1' => [ 'testMarker' => '/* testInvalidUnsetProperty */', 'testType' => 'T_PUBLIC', 'testContent' => 'public', ], - 'property, invalid case 2' => [ + 'property, invalid case 2' => [ 'testMarker' => '/* testInvalidSpaceProperty */', 'testType' => 'T_PUBLIC', 'testContent' => 'public', ], - 'property, invalid case 3' => [ + 'property, invalid case 3' => [ 'testMarker' => '/* testInvalidCommentProperty */', 'testType' => 'T_PROTECTED', 'testContent' => 'protected', ], - 'property, invalid case 4' => [ + 'property, invalid case 4' => [ 'testMarker' => '/* testInvalidGetProperty */', 'testType' => 'T_PRIVATE', 'testContent' => 'private', ], - 'property, invalid case 5' => [ + 'property, invalid case 5' => [ 'testMarker' => '/* testInvalidNoParenProperty */', 'testType' => 'T_PRIVATE', 'testContent' => 'private', ], // Constructor property promotion. - 'promotion, invalid case 1' => [ + 'promotion, invalid case 1' => [ 'testMarker' => '/* testInvalidUnsetCPP */', 'testType' => 'T_PUBLIC', 'testContent' => 'public', ], - 'promotion, invalid case 2' => [ + 'promotion, invalid case 2' => [ 'testMarker' => '/* testInvalidSpaceCPP */', 'testType' => 'T_PUBLIC', 'testContent' => 'public', ], - 'promotion, invalid case 3' => [ + 'promotion, invalid case 3' => [ 'testMarker' => '/* testInvalidCommentCPP */', 'testType' => 'T_PROTECTED', 'testContent' => 'protected', ], - 'promotion, invalid case 4' => [ + 'promotion, invalid case 4' => [ 'testMarker' => '/* testInvalidGetCPP */', 'testType' => 'T_PRIVATE', 'testContent' => 'private', ], - 'promotion, invalid case 5' => [ + 'promotion, invalid case 5' => [ 'testMarker' => '/* testInvalidNoParenCPP */', 'testType' => 'T_PRIVATE', 'testContent' => 'private', ], // Context sensitivitiy. - 'protected as function name' => [ + 'protected as function name' => [ 'testMarker' => '/* testProtectedFunctionName */', 'testType' => 'T_STRING', 'testContent' => 'protected', ], - 'public as function name' => [ + 'public as function name' => [ 'testMarker' => '/* testPublicFunctionName */', 'testType' => 'T_STRING', 'testContent' => 'public', ], - 'set as parameter type' => [ + 'set as parameter type' => [ 'testMarker' => '/* testSetParameterType */', 'testType' => 'T_STRING', 'testContent' => 'Set', ], // Live coding. - 'live coding' => [ + 'live coding' => [ 'testMarker' => '/* testLiveCoding */', 'testType' => 'T_PRIVATE', 'testContent' => 'private', ], ]; - }//end dataAsymmetricVisibility() + }//end dataNotAsymmetricVisibility() }//end class From dc928eee3062dc6de68da2a0d73ff29939f13a97 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 31 Mar 2025 12:51:36 -0700 Subject: [PATCH 08/20] Add tests for existing sniffs --- .../Tests/PHP/LowerCaseConstantUnitTest.1.inc | 12 ++++ .../PHP/LowerCaseConstantUnitTest.1.inc.fixed | 12 ++++ .../Tests/PHP/LowerCaseConstantUnitTest.php | 4 ++ .../Properties/ConstantVisibilityUnitTest.inc | 12 ++++ .../Classes/PropertyDeclarationUnitTest.inc | 14 ++++ .../PropertyDeclarationUnitTest.inc.fixed | 14 ++++ .../Classes/PropertyDeclarationUnitTest.php | 65 ++++++++++--------- 7 files changed, 103 insertions(+), 30 deletions(-) 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/Tests/Properties/ConstantVisibilityUnitTest.inc b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc index 84ea24b2e8..d540b9c2ec 100644 --- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc +++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc @@ -20,3 +20,15 @@ enum SomeEnum { public const BAR = 'bar'; const BAZ = 'baz'; } + +// Don't break on asymmetric visibility +class WithAsym { + + private(set) string $asym1; + + public private(set) string $asym2; + + protected(set) string $asym3; + + public protected(set) string $asym4; +} 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..047300e178 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; + + public protected(set) int $wrongOrder1; + + protected private(set) ?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() From 7d792caf96eceebd9d6cb418cc30e3cfa86c21eb Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 31 Mar 2025 12:56:13 -0700 Subject: [PATCH 09/20] Asymmetric visibility not yet in member properties --- .../PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed | 4 ++-- .../PSR2/Tests/Classes/PropertyDeclarationUnitTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed index 047300e178..efb6a0da00 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed @@ -103,7 +103,7 @@ class AsymmetricVisibility { protected(set) array $unfixed; - public protected(set) int $wrongOrder1; + protected(set) public int $wrongOrder1; - protected private(set) ?string $wrongOrder2; + private(set) protected ?string $wrongOrder2; } diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php index 70abcec928..7b90b70fd7 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php @@ -56,7 +56,7 @@ public function getErrorList() 82 => 1, 84 => 1, 86 => 1, - 90 => 1, + 90 => 2, 94 => 1, 95 => 1, 96 => 1, From 49512e64b98164eef815194c955e2393c439c862 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sat, 5 Apr 2025 21:27:31 -0700 Subject: [PATCH 10/20] Nullable tests --- tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc | 6 ++++++ tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php | 2 ++ 2 files changed, 8 insertions(+) diff --git a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc index 4c018af631..c771175039 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; + + /* testNullablePrivateSetOnly */ + private(set) ?int $prop2; + + /* testNullablePublicPrivateSetOnly */ + public private(set) ?int $prop3; } class InlineThen diff --git a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php index fc7ed4b105..257378f331 100644 --- a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php +++ b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php @@ -51,6 +51,8 @@ public static function dataNullable() { return [ 'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'], + 'property declaration, private set' => ['/* testNullablePrivateSetOnly */'], + 'property declaration, public and private set' => ['/* testNullablePublicPrivateSetOnly */'], ]; }//end dataNullable() From 387063d351b9209b6df5c3a69441f0e5ea3369be Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sat, 5 Apr 2025 21:34:30 -0700 Subject: [PATCH 11/20] Nullable fix --- src/Tokenizers/PHP.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index e6bc072235..b67eafef97 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -2235,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 ) { From 01077e38fc20c6a381c7081e0c95848c0d70e772 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 15 Apr 2025 21:09:04 -0700 Subject: [PATCH 12/20] Accidental change reverted --- .../PSR2/Tests/Classes/PropertyDeclarationUnitTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php index 7b90b70fd7..70abcec928 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php @@ -56,7 +56,7 @@ public function getErrorList() 82 => 1, 84 => 1, 86 => 1, - 90 => 2, + 90 => 1, 94 => 1, 95 => 1, 96 => 1, From 1b5eacb0dad60436ea7f8902756c6fe8ec3aae0b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 06:36:28 +0200 Subject: [PATCH 13/20] TokenNameTest: add new tokens Related to 942 --- tests/Core/Util/Tokens/TokenNameTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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() From 3bc7291631c9677db976cc5c070251b35277db3c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 06:58:47 +0200 Subject: [PATCH 14/20] BackfillAsymmetricVisibilityTest: various small tweaks * Add copyright tag. * Move `@covers` tag to class docblock in anticipation of support for higher PHPUnit versions. * Method order: closely couple the tests with the associated data providers. --- .../PHP/BackfillAsymmetricVisibilityTest.inc | 1 + .../PHP/BackfillAsymmetricVisibilityTest.php | 84 ++++++++++--------- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc index 161a93e853..aaf050ad53 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.inc @@ -55,5 +55,6 @@ class NonVisibilityCases { ) {} } +// Intentional parse error. This must be the last test in the file. class LiveCodingDemo { /* testLiveCoding */ private(set diff --git a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php index deb66fb5fa..13ac4ce410 100644 --- a/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php +++ b/tests/Core/Tokenizers/PHP/BackfillAsymmetricVisibilityTest.php @@ -2,14 +2,20 @@ /** * Tests the support of PHP 8.4 asymmetric visibility. * - * @author Daniel Scherzer - * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @author Daniel Scherzer + * @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 { @@ -22,7 +28,6 @@ final class BackfillAsymmetricVisibilityTest extends AbstractTokenizerTestCase * @param string $testContent The token content to look for * * @dataProvider dataAsymmetricVisibility - * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ @@ -58,43 +63,6 @@ public function testAsymmetricVisibility($testMarker, $testType, $testContent) }//end testAsymmetricVisibility() - /** - * 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 - * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional - * - * @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. * @@ -233,6 +201,42 @@ public static function dataAsymmetricVisibility() }//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. * From 78d1645de6cbe450f0e8f2f8875739f53abd4dfd Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 07:20:36 +0200 Subject: [PATCH 15/20] Tokens::$contextSensitiveKeywords: remove asym keywords These tokens are not single word keywords, so do not belong in this list. Includes updating the test code to allow for the new tests to still pass. --- src/Util/Tokens.php | 3 --- .../Tokenizers/PHP/ContextSensitiveKeywordsTest.php | 10 +++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php index ee01f00603..e2a64a4515 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -747,11 +747,8 @@ final class Tokens T_NEW => T_NEW, T_PRINT => T_PRINT, T_PRIVATE => T_PRIVATE, - T_PRIVATE_SET => T_PRIVATE_SET, T_PROTECTED => T_PROTECTED, - T_PROTECTED_SET => T_PROTECTED_SET, T_PUBLIC => T_PUBLIC, - T_PUBLIC_SET => T_PUBLIC_SET, T_READONLY => T_READONLY, T_REQUIRE => T_REQUIRE, T_REQUIRE_ONCE => T_REQUIRE_ONCE, diff --git a/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizers/PHP/ContextSensitiveKeywordsTest.php index 1b6f871393..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( From c08f84ebe1498dd6f6f2675256d2721b6497f66b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 09:40:47 +0200 Subject: [PATCH 16/20] NullableVsInlineThenTest: minor tweaks * Let the two tests use different asyn visibility tokens. * Fix up the test names - the "Only" at the end of the `testNullableReadonlyOnly` was about the fact that that test didn't have visibility declared, which is not applicable for the new tests. --- tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc | 6 +++--- tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc index c771175039..56ad3856c3 100644 --- a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc +++ b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.inc @@ -5,11 +5,11 @@ class Nullable /* testNullableReadonlyOnly */ readonly ?int $prop; - /* testNullablePrivateSetOnly */ + /* testNullablePrivateSet */ private(set) ?int $prop2; - /* testNullablePublicPrivateSetOnly */ - public private(set) ?int $prop3; + /* 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 257378f331..88920b5601 100644 --- a/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php +++ b/tests/Core/Tokenizers/PHP/NullableVsInlineThenTest.php @@ -50,9 +50,9 @@ public function testNullable($testMarker) public static function dataNullable() { return [ - 'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'], - 'property declaration, private set' => ['/* testNullablePrivateSetOnly */'], - 'property declaration, public and private set' => ['/* testNullablePublicPrivateSetOnly */'], + 'property declaration, readonly, no visibility' => ['/* testNullableReadonlyOnly */'], + 'property declaration, private set' => ['/* testNullablePrivateSet */'], + 'property declaration, public and protected set' => ['/* testNullablePublicProtectedSet */'], ]; }//end dataNullable() From 47b2589107fe422f49b25b00a6aba479a553e943 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 09:52:24 +0200 Subject: [PATCH 17/20] PSR12/ConstantVisibility: remove tests which wouldn't trigger the sniff The sniff is listening for `T_CONST`, so wouldn't trigger on these tests with properties. --- .../Tests/Properties/ConstantVisibilityUnitTest.inc | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc index d540b9c2ec..84ea24b2e8 100644 --- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc +++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc @@ -20,15 +20,3 @@ enum SomeEnum { public const BAR = 'bar'; const BAZ = 'baz'; } - -// Don't break on asymmetric visibility -class WithAsym { - - private(set) string $asym1; - - public private(set) string $asym2; - - protected(set) string $asym3; - - public protected(set) string $asym4; -} From bd1b0d61c39f9b66206f0d5c3f402d54985a5898 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 09:53:22 +0200 Subject: [PATCH 18/20] PSR12/ConstantVisibility: rename test case file --- ...t.inc => ConstantVisibilityUnitTest.1.inc} | 0 .../Properties/ConstantVisibilityUnitTest.php | 20 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) rename src/Standards/PSR12/Tests/Properties/{ConstantVisibilityUnitTest.inc => ConstantVisibilityUnitTest.1.inc} (100%) 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.php b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php index 6c39e7cc6b..9d785e42f6 100644 --- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php +++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php @@ -41,15 +41,23 @@ public function getErrorList() * The key of the array should represent the line number and the value * should represent the number of warnings that should occur on that line. * + * @param string $testFile The name of the file being tested. + * * @return array */ - 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, + ]; + + default: + return []; + } }//end getWarningList() From b010b6a1ae3e0bef9b4a00baa989072b441699a2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 09:59:24 +0200 Subject: [PATCH 19/20] PSR12/ConstantVisibility: prevent false negatives for asym visibility Class constants do not support asymmetric visibility, so if this "typo" would be encountered, the sniff should still flag the constant as not having visibility. Includes test. --- .../PSR12/Sniffs/Properties/ConstantVisibilitySniff.php | 8 +++++++- .../Tests/Properties/ConstantVisibilityUnitTest.2.inc | 7 +++++++ .../PSR12/Tests/Properties/ConstantVisibilityUnitTest.php | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.2.inc 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.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 @@ + 1, ]; + case 'ConstantVisibilityUnitTest.2.inc': + return [ + 6 => 1, + ]; + default: return []; } From 0a4dd1c26fd7a9fe684fc03fc18c562f1f6ba21e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 10 May 2025 10:38:25 +0200 Subject: [PATCH 20/20] CS fix --- src/Util/Tokens.php | 140 ++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php index e2a64a4515..db9a2735b6 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -694,76 +694,76 @@ final class Tokens * https://wiki.php.net/rfc/context_sensitive_lexer */ public static $contextSensitiveKeywords = [ - T_ABSTRACT => T_ABSTRACT, - T_ARRAY => T_ARRAY, - T_AS => T_AS, - T_BREAK => T_BREAK, - T_CALLABLE => T_CALLABLE, - T_CASE => T_CASE, - T_CATCH => T_CATCH, - T_CLASS => T_CLASS, - T_CLONE => T_CLONE, - T_CONST => T_CONST, - T_CONTINUE => T_CONTINUE, - T_DECLARE => T_DECLARE, - T_DEFAULT => T_DEFAULT, - T_DO => T_DO, - T_ECHO => T_ECHO, - T_ELSE => T_ELSE, - T_ELSEIF => T_ELSEIF, - T_EMPTY => T_EMPTY, - T_ENDDECLARE => T_ENDDECLARE, - T_ENDFOR => T_ENDFOR, - T_ENDFOREACH => T_ENDFOREACH, - T_ENDIF => T_ENDIF, - T_ENDSWITCH => T_ENDSWITCH, - T_ENDWHILE => T_ENDWHILE, - T_ENUM => T_ENUM, - T_EVAL => T_EVAL, - T_EXIT => T_EXIT, - T_EXTENDS => T_EXTENDS, - T_FINAL => T_FINAL, - T_FINALLY => T_FINALLY, - T_FN => T_FN, - T_FOR => T_FOR, - T_FOREACH => T_FOREACH, - T_FUNCTION => T_FUNCTION, - T_GLOBAL => T_GLOBAL, - T_GOTO => T_GOTO, - T_IF => T_IF, - T_IMPLEMENTS => T_IMPLEMENTS, - T_INCLUDE => T_INCLUDE, - T_INCLUDE_ONCE => T_INCLUDE_ONCE, - T_INSTANCEOF => T_INSTANCEOF, - T_INSTEADOF => T_INSTEADOF, - T_INTERFACE => T_INTERFACE, - T_ISSET => T_ISSET, - T_LIST => T_LIST, - T_LOGICAL_AND => T_LOGICAL_AND, - T_LOGICAL_OR => T_LOGICAL_OR, - T_LOGICAL_XOR => T_LOGICAL_XOR, - T_MATCH => T_MATCH, - T_NAMESPACE => T_NAMESPACE, - T_NEW => T_NEW, - T_PRINT => T_PRINT, - T_PRIVATE => T_PRIVATE, - T_PROTECTED => T_PROTECTED, - T_PUBLIC => T_PUBLIC, - T_READONLY => T_READONLY, - T_REQUIRE => T_REQUIRE, - T_REQUIRE_ONCE => T_REQUIRE_ONCE, - T_RETURN => T_RETURN, - T_STATIC => T_STATIC, - T_SWITCH => T_SWITCH, - T_THROW => T_THROW, - T_TRAIT => T_TRAIT, - T_TRY => T_TRY, - T_UNSET => T_UNSET, - T_USE => T_USE, - T_VAR => T_VAR, - T_WHILE => T_WHILE, - T_YIELD => T_YIELD, - T_YIELD_FROM => T_YIELD_FROM, + T_ABSTRACT => T_ABSTRACT, + T_ARRAY => T_ARRAY, + T_AS => T_AS, + T_BREAK => T_BREAK, + T_CALLABLE => T_CALLABLE, + T_CASE => T_CASE, + T_CATCH => T_CATCH, + T_CLASS => T_CLASS, + T_CLONE => T_CLONE, + T_CONST => T_CONST, + T_CONTINUE => T_CONTINUE, + T_DECLARE => T_DECLARE, + T_DEFAULT => T_DEFAULT, + T_DO => T_DO, + T_ECHO => T_ECHO, + T_ELSE => T_ELSE, + T_ELSEIF => T_ELSEIF, + T_EMPTY => T_EMPTY, + T_ENDDECLARE => T_ENDDECLARE, + T_ENDFOR => T_ENDFOR, + T_ENDFOREACH => T_ENDFOREACH, + T_ENDIF => T_ENDIF, + T_ENDSWITCH => T_ENDSWITCH, + T_ENDWHILE => T_ENDWHILE, + T_ENUM => T_ENUM, + T_EVAL => T_EVAL, + T_EXIT => T_EXIT, + T_EXTENDS => T_EXTENDS, + T_FINAL => T_FINAL, + T_FINALLY => T_FINALLY, + T_FN => T_FN, + T_FOR => T_FOR, + T_FOREACH => T_FOREACH, + T_FUNCTION => T_FUNCTION, + T_GLOBAL => T_GLOBAL, + T_GOTO => T_GOTO, + T_IF => T_IF, + T_IMPLEMENTS => T_IMPLEMENTS, + T_INCLUDE => T_INCLUDE, + T_INCLUDE_ONCE => T_INCLUDE_ONCE, + T_INSTANCEOF => T_INSTANCEOF, + T_INSTEADOF => T_INSTEADOF, + T_INTERFACE => T_INTERFACE, + T_ISSET => T_ISSET, + T_LIST => T_LIST, + T_LOGICAL_AND => T_LOGICAL_AND, + T_LOGICAL_OR => T_LOGICAL_OR, + T_LOGICAL_XOR => T_LOGICAL_XOR, + T_MATCH => T_MATCH, + T_NAMESPACE => T_NAMESPACE, + T_NEW => T_NEW, + T_PRINT => T_PRINT, + T_PRIVATE => T_PRIVATE, + T_PROTECTED => T_PROTECTED, + T_PUBLIC => T_PUBLIC, + T_READONLY => T_READONLY, + T_REQUIRE => T_REQUIRE, + T_REQUIRE_ONCE => T_REQUIRE_ONCE, + T_RETURN => T_RETURN, + T_STATIC => T_STATIC, + T_SWITCH => T_SWITCH, + T_THROW => T_THROW, + T_TRAIT => T_TRAIT, + T_TRY => T_TRY, + T_UNSET => T_UNSET, + T_USE => T_USE, + T_VAR => T_VAR, + T_WHILE => T_WHILE, + T_YIELD => T_YIELD, + T_YIELD_FROM => T_YIELD_FROM, ];