diff --git a/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.1.inc b/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.1.inc index a4b0a07f9d..38034c0dee 100644 --- a/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.1.inc +++ b/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.1.inc @@ -98,3 +98,7 @@ $var = (boolean)/* comment */ $var2; $var = ( int )$spacesInsideParenthesis; $var = ( int )$tabsInsideParenthesis; + +$var = (void) callMe(); +$var = (void)callMe(); +$var = (void) callMe(); diff --git a/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.1.inc.fixed b/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.1.inc.fixed index fd2b920dcb..b8a6068bf2 100644 --- a/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.1.inc.fixed +++ b/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.1.inc.fixed @@ -95,3 +95,7 @@ $var = (boolean)/* comment */ $var2; $var = ( int ) $spacesInsideParenthesis; $var = ( int ) $tabsInsideParenthesis; + +$var = (void) callMe(); +$var = (void) callMe(); +$var = (void) callMe(); diff --git a/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.php b/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.php index 853826ca20..68f5f68920 100644 --- a/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.php +++ b/src/Standards/Generic/Tests/Formatting/SpaceAfterCastUnitTest.php @@ -77,6 +77,8 @@ public function getErrorList($testFile = '') 97 => 1, 99 => 1, 100 => 1, + 103 => 1, + 104 => 1, ]; default: diff --git a/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.inc b/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.inc index 905101d069..4cb84e9360 100644 --- a/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.inc +++ b/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.inc @@ -63,3 +63,7 @@ $var = array( ); (bool) $a ? echo $b : echo $c; + +$var = (void) callMe(); +$var =(void) callMe(); +$var = (void) callMe(); diff --git a/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.inc.fixed b/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.inc.fixed index 8b60c904ab..1eb6863033 100644 --- a/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.inc.fixed @@ -63,3 +63,7 @@ $var = array( ); (bool) $a ? echo $b : echo $c; + +$var = (void) callMe(); +$var = (void) callMe(); +$var = (void) callMe(); diff --git a/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.php b/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.php index dcf4e783f4..22c9108754 100644 --- a/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.php +++ b/src/Standards/Generic/Tests/Formatting/SpaceBeforeCastUnitTest.php @@ -61,6 +61,8 @@ public function getErrorList() 53 => 1, 55 => 1, 56 => 1, + 68 => 1, + 69 => 1, ]; } diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.1.inc b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.1.inc index a3dc0f1730..62c5c543d5 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.1.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.1.inc @@ -142,3 +142,5 @@ class DNFTypes { interface PHP84HookedProperty { public String $readable { get; } } + +$php85 = (VOID) callMe(); diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.1.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.1.inc.fixed index f33eacefaf..116befb131 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.1.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.1.inc.fixed @@ -142,3 +142,5 @@ class DNFTypes { interface PHP84HookedProperty { public string $readable { get; } } + +$php85 = (void) callMe(); diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php index 9c7705945d..f982627592 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php @@ -99,6 +99,7 @@ public function getErrorList($testFile = '') 136 => 1, 139 => 2, 143 => 1, + 146 => 1, ]; default: diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc index b2dd790f9c..221d260a7b 100644 --- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc @@ -205,3 +205,5 @@ $cntPages = ceil(count($items) / parent::ON_PAGE); $nsRelative = namespace\doSomething($items + 10); $partiallyQualified = Partially\qualified($items + 10); $fullyQualified = \Fully\qualified($items + 10); + +$php85voidcast = (void) CallMeOnce() + (void) CallMeTwice(); diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc.fixed index fe7d4ef3d3..932093429a 100644 --- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc.fixed +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.1.inc.fixed @@ -205,3 +205,5 @@ $cntPages = ceil(count($items) / parent::ON_PAGE); $nsRelative = namespace\doSomething($items + 10); $partiallyQualified = Partially\qualified($items + 10); $fullyQualified = \Fully\qualified($items + 10); + +$php85voidcast = ((void) CallMeOnce() + (void) CallMeTwice()); diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php index 0d5e5bb72f..610de4554c 100644 --- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php @@ -78,6 +78,7 @@ public function getErrorList($testFile = '') 189 => 1, 193 => 1, 194 => 2, + 209 => 1, ]; default: diff --git a/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.inc index fa65112fbe..31b3738a4a 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.inc @@ -7,3 +7,5 @@ $var = ( int ) $var2; $var = (binary) $var2; $var = ( binary ) $var2; + +$php85 = ( void) callMe(); diff --git a/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.inc.fixed index 2fb37bba48..5d944477ea 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.inc.fixed @@ -7,3 +7,5 @@ $var = (int) $var2; $var = (binary) $var2; $var = (binary) $var2; + +$php85 = (void) callMe(); diff --git a/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.php index cd47380946..cf9aff8779 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.php +++ b/src/Standards/Squiz/Tests/WhiteSpace/CastSpacingUnitTest.php @@ -32,11 +32,12 @@ final class CastSpacingUnitTest extends AbstractSniffTestCase public function getErrorList() { return [ - 3 => 1, - 4 => 1, - 5 => 1, - 6 => 1, - 9 => 1, + 3 => 1, + 4 => 1, + 5 => 1, + 6 => 1, + 9 => 1, + 11 => 1, ]; } diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 926d7d0724..13aaeab00e 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -1122,6 +1122,58 @@ protected function tokenize(string $code) continue; } + /* + Detect PHP 8.5+ void casting and assign the casts their own token. + + Mind: type cast tokens _may_ contain whitespace, but no new lines and no comments. + */ + + if (PHP_VERSION_ID < 80500 + && $token[0] === '(' + ) { + $content = $token[0]; + $i = ($stackPtr + 1); + + if (is_array($tokens[$i]) === true + && $tokens[$i][0] === T_WHITESPACE + && strpos($tokens[$i][1], "\n") === false + && strpos($tokens[$i][1], "\r") === false + ) { + $content .= $tokens[$i][1]; + ++$i; + } + + if (is_array($tokens[$i]) === true + && $tokens[$i][0] === T_STRING + && strtolower($tokens[$i][1]) === 'void' + ) { + $content .= $tokens[$i][1]; + ++$i; + + if (is_array($tokens[$i]) === true + && $tokens[$i][0] === T_WHITESPACE + && strpos($tokens[$i][1], "\n") === false + && strpos($tokens[$i][1], "\r") === false + ) { + $content .= $tokens[$i][1]; + ++$i; + } + + if ($tokens[$i][0] === ')') { + $content .= $tokens[$i][0]; + + $finalTokens[$newStackPtr] = [ + 'code' => T_VOID_CAST, + 'type' => 'T_VOID_CAST', + 'content' => $content, + ]; + $newStackPtr++; + $stackPtr = $i; + continue; + } + } + } + /* If this is a heredoc, PHP will tokenize the whole thing which causes problems when heredocs don't diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php index 8b20521c4f..62c918c31b 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -143,6 +143,11 @@ define('T_PRIVATE_SET', 'PHPCS_T_PRIVATE_SET'); } +// Some PHP 8.5 tokens, replicated for lower versions. +if (defined('T_VOID_CAST') === false) { + define('T_VOID_CAST', 'PHPCS_T_VOID_CAST'); +} + // 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'); @@ -266,6 +271,7 @@ final class Tokens T_OBJECT_CAST => T_OBJECT_CAST, T_UNSET_CAST => T_UNSET_CAST, T_BINARY_CAST => T_BINARY_CAST, + T_VOID_CAST => T_VOID_CAST, ]; /** diff --git a/tests/Core/Tokenizers/PHP/TypecastsTest.inc b/tests/Core/Tokenizers/PHP/TypecastsTest.inc new file mode 100644 index 0000000000..8e0a4c9b42 --- /dev/null +++ b/tests/Core/Tokenizers/PHP/TypecastsTest.inc @@ -0,0 +1,130 @@ +> $expectedTokens The tokenization expected. + * + * @dataProvider dataNotATypeCast + * + * @return void + */ + public function testNotATypeCast($testMarker, $expectedTokens) + { + $tokens = $this->phpcsFile->getTokens(); + $target = $this->getTargetToken($testMarker, [T_OPEN_PARENTHESIS]); + + foreach ($expectedTokens as $nr => $tokenInfo) { + $this->assertSame( + constant($tokenInfo['type']), + $tokens[$target]['code'], + 'Token tokenized as ' . Tokens::tokenName($tokens[$target]['code']) . ', not ' . $tokenInfo['type'] . ' (code)' + ); + $this->assertSame( + $tokenInfo['type'], + $tokens[$target]['type'], + 'Token tokenized as ' . $tokens[$target]['type'] . ', not ' . $tokenInfo['type'] . ' (type)' + ); + $this->assertSame( + $tokenInfo['content'], + $tokens[$target]['content'], + 'Content of token ' . ($nr + 1) . ' (' . $tokens[$target]['type'] . ') does not match expectations' + ); + + ++$target; + } + } + + + /** + * Data provider. + * + * @see testNotATypeCast() + * + * @return array>>> + */ + public static function dataNotATypeCast() + { + return [ + 'constant within parentheses' => [ + 'testMarker' => '/* testNotATypeCast1 */', + 'expectedTokens' => [ + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + [ + 'type' => 'T_STRING', + 'content' => 'NOT_A_TYPECAST', + ], + [ + 'type' => 'T_CLOSE_PARENTHESIS', + 'content' => ')', + ], + ], + ], + 'Invalid type cast void - new lines are not allowed [1]' => [ + 'testMarker' => '/* testNotATypeCast2 */', + 'expectedTokens' => [ + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_STRING', + 'content' => 'void', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_CLOSE_PARENTHESIS', + 'content' => ')', + ], + ], + ], + 'Invalid type cast void - new lines are not allowed [2]' => [ + 'testMarker' => '/* testNotATypeCast3 */', + 'expectedTokens' => [ + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'void', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_CLOSE_PARENTHESIS', + 'content' => ')', + ], + ], + ], + 'Invalid type cast void - comments are not allowed' => [ + 'testMarker' => '/* testNotATypeCast4 */', + 'expectedTokens' => [ + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '/*comment*/', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'void', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '/*comment */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_CLOSE_PARENTHESIS', + 'content' => ')', + ], + ], + ], + 'Live coding/parse error' => [ + 'testMarker' => '/* testNotATypeCast5 */', + 'expectedTokens' => [ + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + [ + 'type' => 'T_STRING', + 'content' => 'void', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => "\n", + ], + ], + ], + ]; + } + + + /** + * Test that valid type casts are tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedType Expected token type. + * @param string $expectedContent Expected token content. + * + * @dataProvider dataTypeCast + * + * @return void + */ + public function testTypeCast($testMarker, $expectedType, $expectedContent) + { + $tokens = $this->phpcsFile->getTokens(); + $expectedCode = constant($expectedType); + $target = $this->getTargetToken($testMarker, $expectedCode); + $tokenArray = $tokens[$target]; + + $this->assertSame($expectedCode, $tokenArray['code'], "Token tokenized as {$tokenArray['type']}, not $expectedType (code)"); + $this->assertSame($expectedType, $tokenArray['type'], "Token tokenized as {$tokenArray['type']}, not $expectedType (type)"); + + if (isset($tokenArray['orig_content']) === true) { + $this->assertSame($expectedContent, $tokenArray['orig_content'], 'Token (orig) content does not match expectation'); + } else { + $this->assertSame($expectedContent, $tokenArray['content'], 'Token content does not match expectation'); + } + + // Make sure there are no stray tokens. + // This assertion requires all type casts tested via this test to be followed whitespace + a variable. + $this->assertSame(T_WHITESPACE, $tokens[($target + 1)]['code'], 'Stray tokens detected'); + $this->assertSame(T_VARIABLE, $tokens[($target + 2)]['code'], 'Stray tokens detected'); + } + + + /** + * Data provider. + * + * @see testTypeCast() + * + * @return array> + */ + public static function dataTypeCast() + { + return [ + '(bool)' => [ + 'testMarker' => '/* testBool */', + 'expectedType' => 'T_BOOL_CAST', + 'expectedContent' => '(bool)', + ], + '( BOOL )' => [ + 'testMarker' => '/* testSpacyUppercaseBool */', + 'expectedType' => 'T_BOOL_CAST', + 'expectedContent' => '( BOOL)', + ], + '(boolean)' => [ + 'testMarker' => '/* testBoolean */', + 'expectedType' => 'T_BOOL_CAST', + 'expectedContent' => '(boolean)', + ], + '( BOOLEAN )' => [ + 'testMarker' => '/* testSpacyUppercaseBoolean */', + 'expectedType' => 'T_BOOL_CAST', + 'expectedContent' => '( BOOLEAN )', + ], + '(int)' => [ + 'testMarker' => '/* testInt */', + 'expectedType' => 'T_INT_CAST', + 'expectedContent' => '(int)', + ], + '( INT )' => [ + 'testMarker' => '/* testSpacyUppercaseInt */', + 'expectedType' => 'T_INT_CAST', + 'expectedContent' => '( INT )', + ], + '(integer)' => [ + 'testMarker' => '/* testInteger */', + 'expectedType' => 'T_INT_CAST', + 'expectedContent' => '(integer)', + ], + '( INTEGER )' => [ + 'testMarker' => '/* testSpacyUppercaseInteger */', + 'expectedType' => 'T_INT_CAST', + 'expectedContent' => '( INTEGER )', + ], + '(float)' => [ + 'testMarker' => '/* testFloat */', + 'expectedType' => 'T_DOUBLE_CAST', + 'expectedContent' => '(float)', + ], + '( FLOAT )' => [ + 'testMarker' => '/* testSpacyUppercaseFloat */', + 'expectedType' => 'T_DOUBLE_CAST', + 'expectedContent' => '( FLOAT )', + ], + '(real)' => [ + 'testMarker' => '/* testReal */', + 'expectedType' => 'T_DOUBLE_CAST', + 'expectedContent' => '(real)', + ], + '( REAL )' => [ + 'testMarker' => '/* testSpacyUppercaseReal */', + 'expectedType' => 'T_DOUBLE_CAST', + 'expectedContent' => '( REAL )', + ], + '(double)' => [ + 'testMarker' => '/* testDouble */', + 'expectedType' => 'T_DOUBLE_CAST', + 'expectedContent' => '(double)', + ], + '( DOUBLE )' => [ + 'testMarker' => '/* testSpacyUppercaseDouble */', + 'expectedType' => 'T_DOUBLE_CAST', + 'expectedContent' => '( DOUBLE )', + ], + '(string)' => [ + 'testMarker' => '/* testString */', + 'expectedType' => 'T_STRING_CAST', + 'expectedContent' => '(string)', + ], + '( STRING )' => [ + 'testMarker' => '/* testSpacyUppercaseString */', + 'expectedType' => 'T_STRING_CAST', + 'expectedContent' => '(STRING )', + ], + '(binary)' => [ + 'testMarker' => '/* testBinary */', + 'expectedType' => 'T_BINARY_CAST', + 'expectedContent' => '(binary)', + ], + '( BINARY )' => [ + 'testMarker' => '/* testSpacyUppercaseBinary */', + 'expectedType' => 'T_BINARY_CAST', + 'expectedContent' => '( BINARY )', + ], + '(array)' => [ + 'testMarker' => '/* testArray */', + 'expectedType' => 'T_ARRAY_CAST', + 'expectedContent' => '(array)', + ], + '( ARRAY )' => [ + 'testMarker' => '/* testSpacyUppercaseArray */', + 'expectedType' => 'T_ARRAY_CAST', + 'expectedContent' => '( ARRAY )', + ], + '(object)' => [ + 'testMarker' => '/* testObject */', + 'expectedType' => 'T_OBJECT_CAST', + 'expectedContent' => '(object)', + ], + '( OBJECT )' => [ + 'testMarker' => '/* testSpacyUppercaseObject */', + 'expectedType' => 'T_OBJECT_CAST', + 'expectedContent' => '( OBJECT )', + ], + '(unset)' => [ + 'testMarker' => '/* testUnset */', + 'expectedType' => 'T_UNSET_CAST', + 'expectedContent' => '(unset)', + ], + '( UNSET )' => [ + 'testMarker' => '/* testSpacyUppercaseUnset */', + 'expectedType' => 'T_UNSET_CAST', + 'expectedContent' => '( UNSET )', + ], + + // PHP 8.5: new (void) cast. + '(void)' => [ + 'testMarker' => '/* testVoid */', + 'expectedType' => 'T_VOID_CAST', + 'expectedContent' => '(void)', + ], + 'Nested (void)' => [ + 'testMarker' => '/* testVoidNested */', + 'expectedType' => 'T_VOID_CAST', + 'expectedContent' => '(void)', + ], + '( void ) (spaces)' => [ + 'testMarker' => '/* testSpacyVoid */', + 'expectedType' => 'T_VOID_CAST', + 'expectedContent' => '( void )', + ], + '(\tvoid\t) (tabs)' => [ + 'testMarker' => '/* testTabbyVoid */', + 'expectedType' => 'T_VOID_CAST', + 'expectedContent' => "(\tvoid\t)", + ], + '(VOID)' => [ + 'testMarker' => '/* testUppercaseVoid */', + 'expectedType' => 'T_VOID_CAST', + 'expectedContent' => '(VOID)', + ], + ]; + } +}