From 42551e53a5d82182d794577ea946dd6c57f3d7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Fri, 19 Nov 2021 20:13:48 +0100 Subject: [PATCH] PHP 8.1: Added support for "enum" keyword --- package.xml | 6 ++ src/Tokenizers/PHP.php | 45 +++++++++ src/Util/Tokens.php | 7 ++ tests/Core/Tokenizer/EnumTest.inc | 88 ++++++++++++++++ tests/Core/Tokenizer/EnumTest.php | 163 ++++++++++++++++++++++++++++++ 5 files changed, 309 insertions(+) create mode 100644 tests/Core/Tokenizer/EnumTest.inc create mode 100644 tests/Core/Tokenizer/EnumTest.php diff --git a/package.xml b/package.xml index d1d4464b77..4aa45c5bdd 100644 --- a/package.xml +++ b/package.xml @@ -133,6 +133,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -2084,6 +2086,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -2174,6 +2178,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 1ba26363af..9d8e45b752 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -152,6 +152,13 @@ class PHP extends Tokenizer 'shared' => false, 'with' => [], ], + T_ENUM => [ + 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET], + 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET], + 'strict' => true, + 'shared' => false, + 'with' => [], + ], T_USE => [ 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET], 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET], @@ -339,6 +346,7 @@ class PHP extends Tokenizer T_ENDIF => 5, T_ENDSWITCH => 9, T_ENDWHILE => 8, + T_ENUM => 4, T_EVAL => 4, T_EXTENDS => 7, T_FILE => 8, @@ -466,6 +474,7 @@ class PHP extends Tokenizer T_CLASS => true, T_INTERFACE => true, T_TRAIT => true, + T_ENUM => true, T_EXTENDS => true, T_IMPLEMENTS => true, T_ATTRIBUTE => true, @@ -870,6 +879,42 @@ protected function tokenize($string) continue; }//end if + /* + Enum keyword for PHP < 8.1 + */ + + if ($tokenIsArray === true + && $token[0] === T_STRING + && strtolower($token[1]) === 'enum' + ) { + // Get the next non-empty token. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === false + || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false + ) { + break; + } + } + + if (isset($tokens[$i]) === true + && is_array($tokens[$i]) === true + && $tokens[$i][0] === T_STRING + ) { + $newToken = []; + $newToken['code'] = T_ENUM; + $newToken['type'] = 'T_ENUM'; + $newToken['content'] = $tokens[$i][1]; + $finalTokens[$newStackPtr] = $newToken; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from T_STRING to T_ENUM".PHP_EOL; + } + + $newStackPtr++; + 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 f501c7f0a4..d5bccc8242 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -154,6 +154,10 @@ define('T_ATTRIBUTE', 'PHPCS_T_ATTRIBUTE'); } +if (defined('T_ENUM') === false) { + define('T_ENUM', 'PHPCS_T_ENUM'); +} + // Some PHP 8.1 tokens, replicated for lower versions. if (defined('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG') === false) { define('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG', 'PHPCS_T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG'); @@ -190,6 +194,7 @@ final class Tokens T_CLASS => 1000, T_INTERFACE => 1000, T_TRAIT => 1000, + T_ENUM => 1000, T_NAMESPACE => 1000, T_FUNCTION => 100, T_CLOSURE => 100, @@ -415,6 +420,7 @@ final class Tokens T_ANON_CLASS => T_ANON_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, T_NAMESPACE => T_NAMESPACE, T_FUNCTION => T_FUNCTION, T_CLOSURE => T_CLOSURE, @@ -629,6 +635,7 @@ final class Tokens T_ANON_CLASS => T_ANON_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, ]; /** diff --git a/tests/Core/Tokenizer/EnumTest.inc b/tests/Core/Tokenizer/EnumTest.inc new file mode 100644 index 0000000000..3501c97445 --- /dev/null +++ b/tests/Core/Tokenizer/EnumTest.inc @@ -0,0 +1,88 @@ +enum = 'foo'; + } +} + +/* testEnumUsedAsFunctionName */ +function enum() +{ +} + +/* testDeclarationContainingComment */ +enum /* comment */ Name +{ + case SOME_CASE; +} + +/* testEnumUsedAsNamespaceName */ +namespace Enum; +/* testEnumUsedAsPartOfNamespaceName */ +namespace My\Enum\Collection; +/* testEnumUsedInObjectInitialization */ +$obj = new Enum; +/* testEnumAsFunctionCall */ +$var = enum($a, $b); +/* testEnumAsFunctionCallWithNamespace */ +var = namespace\enum(); +/* testClassConstantFetchWithEnumAsClassName */ +echo Enum::CONSTANT; +/* testClassConstantFetchWithEnumAsConstantName */ +echo ClassName::ENUM; + +/* testParseErrorMissingName */ +enum { + case SOME_CASE; +} + +/* testParseErrorLiveCoding */ +// This must be the last test in the file. +enum diff --git a/tests/Core/Tokenizer/EnumTest.php b/tests/Core/Tokenizer/EnumTest.php new file mode 100644 index 0000000000..ea818924a5 --- /dev/null +++ b/tests/Core/Tokenizer/EnumTest.php @@ -0,0 +1,163 @@ + + * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class EnumTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the "enum" keyword is tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int $expectedScopeOpenerLine Expected line of scope opener. + * @param int $expectedScopeCloserLine Expected line of scope closer. + * + * @dataProvider dataEnums + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testEnums($testMarker, $expectedScopeOpenerLine, $expectedScopeCloserLine) + { + $tokens = self::$phpcsFile->getTokens(); + + $enum = $this->getTargetToken($testMarker, [T_ENUM, T_STRING]); + + $this->assertSame(T_ENUM, $tokens[$enum]['code']); + $this->assertSame('T_ENUM', $tokens[$enum]['type']); + + $this->assertArrayHasKey('scope_condition', $tokens[$enum]); + $this->assertArrayHasKey('scope_opener', $tokens[$enum]); + $this->assertArrayHasKey('scope_closer', $tokens[$enum]); + + $this->assertSame($enum, $tokens[$enum]['scope_condition']); + + $scopeOpener = $tokens[$enum]['scope_opener']; + $scopeCloser = $tokens[$enum]['scope_closer']; + + $this->assertSame($expectedScopeOpenerLine, $tokens[$scopeOpener]['line']); + $this->assertArrayHasKey('scope_condition', $tokens[$scopeOpener]); + $this->assertArrayHasKey('scope_opener', $tokens[$scopeOpener]); + $this->assertArrayHasKey('scope_closer', $tokens[$scopeOpener]); + $this->assertSame($enum, $tokens[$scopeOpener]['scope_condition']); + $this->assertSame($scopeOpener, $tokens[$scopeOpener]['scope_opener']); + $this->assertSame($scopeCloser, $tokens[$scopeOpener]['scope_closer']); + + $this->assertSame($expectedScopeCloserLine, $tokens[$scopeCloser]['line']); + $this->assertArrayHasKey('scope_condition', $tokens[$scopeCloser]); + $this->assertArrayHasKey('scope_opener', $tokens[$scopeCloser]); + $this->assertArrayHasKey('scope_closer', $tokens[$scopeCloser]); + $this->assertSame($enum, $tokens[$scopeCloser]['scope_condition']); + $this->assertSame($scopeOpener, $tokens[$scopeCloser]['scope_opener']); + $this->assertSame($scopeCloser, $tokens[$scopeCloser]['scope_closer']); + + }//end testEnums() + + + /** + * Data provider. + * + * @see testEnums() + * + * @return array + */ + public function dataEnums() + { + return [ + [ + '/* testPureEnum */', + 5, + 7, + ], + [ + '/* testBackedIntEnum */', + 10, + 13, + ], + [ + '/* testBackedStringEnum */', + 17, + 20, + ], + [ + '/* testComplexEnum */', + 24, + 36, + ], + [ + '/* testEnumWithEnumAsClassName */', + 39, + 39, + ], + [ + '/* testDeclarationContainingComment */', + 62, + 64, + ], + ]; + + }//end dataEnums() + + + /** + * Test that "enum" when not used as the keyword is still tokenized as `T_STRING`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataNotEnums + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testNotEnums($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_ENUM, T_STRING]); + $this->assertSame(T_STRING, $tokens[$target]['code']); + $this->assertSame('T_STRING', $tokens[$target]['type']); + + }//end testNotEnums() + + + /** + * Data provider. + * + * @see testNotEnums() + * + * @return array + */ + public function dataNotEnums() + { + return [ + ['/* testEnumAsClassNameAfterEnumKeyword */'], + ['/* testEnumUsedAsClassName */'], + ['/* testEnumUsedAsClassConstantName */'], + ['/* testEnumUsedAsMethodName */'], + ['/* testEnumUsedAsPropertyName */'], + ['/* testEnumUsedAsFunctionName */'], + ['/* testEnumUsedAsNamespaceName */'], + ['/* testEnumUsedAsPartOfNamespaceName */'], + ['/* testEnumUsedInObjectInitialization */'], + ['/* testEnumAsFunctionCall */'], + ['/* testEnumAsFunctionCallWithNamespace */'], + ['/* testClassConstantFetchWithEnumAsClassName */'], + ['/* testClassConstantFetchWithEnumAsConstantName */'], + ['/* testParseErrorMissingName */'], + ['/* testParseErrorLiveCoding */'], + ]; + + }//end dataNotEnums() + + +}//end class