diff --git a/package.xml b/package.xml index f3ceca5c0b..fa24f0c587 100644 --- a/package.xml +++ b/package.xml @@ -131,6 +131,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -2092,6 +2094,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -2188,6 +2192,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index c8efba537d..7f24a86af0 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, @@ -467,6 +475,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, @@ -952,6 +961,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'] = $token[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 b05eb6161a..64014a17e0 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -167,6 +167,10 @@ define('T_READONLY', 'PHPCS_T_READONLY'); } +if (defined('T_ENUM') === false) { + define('T_ENUM', 'PHPCS_T_ENUM'); +} + // 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'); @@ -194,6 +198,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, @@ -419,6 +424,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, @@ -633,6 +639,7 @@ final class Tokens T_ANON_CLASS => T_ANON_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, ]; /** @@ -684,6 +691,7 @@ final class Tokens T_ENDIF => T_ENDIF, T_ENDSWITCH => T_ENDSWITCH, T_ENDWHILE => T_ENDWHILE, + T_ENUM => T_ENUM, T_EXIT => T_EXIT, T_EXTENDS => T_EXTENDS, T_FINAL => T_FINAL, diff --git a/tests/Core/Tokenizer/BackfillEnumTest.inc b/tests/Core/Tokenizer/BackfillEnumTest.inc new file mode 100644 index 0000000000..28feb2f28c --- /dev/null +++ b/tests/Core/Tokenizer/BackfillEnumTest.inc @@ -0,0 +1,95 @@ +enum = 'foo'; + } +} + +/* testEnumUsedAsFunctionName */ +function enum() +{ +} + +/* testDeclarationContainingComment */ +enum /* comment */ Name +{ + case SOME_CASE; +} + +enum /* testEnumUsedAsEnumName */ Enum +{ +} + +/* 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/BackfillEnumTest.php b/tests/Core/Tokenizer/BackfillEnumTest.php new file mode 100644 index 0000000000..8653e8c1b9 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillEnumTest.php @@ -0,0 +1,229 @@ + + * @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 BackfillEnumTest 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 string $testContent The token content to look for. + * @param int $openerOffset Offset to find expected scope opener. + * @param int $closerOffset Offset to find expected scope closer. + * + * @dataProvider dataEnums + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testEnums($testMarker, $testContent, $openerOffset, $closerOffset) + { + $tokens = self::$phpcsFile->getTokens(); + + $enum = $this->getTargetToken($testMarker, [T_ENUM, T_STRING], $testContent); + + $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']; + + $expectedScopeOpener = ($enum + $openerOffset); + $expectedScopeCloser = ($enum + $closerOffset); + + $this->assertSame($expectedScopeOpener, $scopeOpener); + $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($expectedScopeCloser, $scopeCloser); + $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 */', + 'enum', + 4, + 12, + ], + [ + '/* testBackedIntEnum */', + 'enum', + 6, + 28, + ], + [ + '/* testBackedStringEnum */', + 'enum', + 6, + 28, + ], + [ + '/* testComplexEnum */', + 'enum', + 10, + 71, + ], + [ + '/* testEnumWithEnumAsClassName */', + 'enum', + 6, + 7, + ], + [ + '/* testEnumIsCaseInsensitive */', + 'EnUm', + 4, + 5, + ], + [ + '/* testDeclarationContainingComment */', + 'enum', + 6, + 14, + ], + ]; + + }//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. + * @param string $testContent The token content to look for. + * + * @dataProvider dataNotEnums + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testNotEnums($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_ENUM, T_STRING], $testContent); + $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 */', + 'Enum', + ], + [ + '/* testEnumUsedAsClassName */', + 'Enum', + ], + [ + '/* testEnumUsedAsClassConstantName */', + 'ENUM', + ], + [ + '/* testEnumUsedAsMethodName */', + 'enum', + ], + [ + '/* testEnumUsedAsPropertyName */', + 'enum', + ], + [ + '/* testEnumUsedAsFunctionName */', + 'enum', + ], + [ + '/* testEnumUsedAsEnumName */', + 'Enum', + ], + [ + '/* testEnumUsedAsNamespaceName */', + 'Enum', + ], + [ + '/* testEnumUsedAsPartOfNamespaceName */', + 'Enum', + ], + [ + '/* testEnumUsedInObjectInitialization */', + 'Enum', + ], + [ + '/* testEnumAsFunctionCall */', + 'enum', + ], + [ + '/* testEnumAsFunctionCallWithNamespace */', + 'enum', + ], + [ + '/* testClassConstantFetchWithEnumAsClassName */', + 'Enum', + ], + [ + '/* testClassConstantFetchWithEnumAsConstantName */', + 'ENUM', + ], + [ + '/* testParseErrorMissingName */', + 'enum', + ], + [ + '/* testParseErrorLiveCoding */', + 'enum', + ], + ]; + + }//end dataNotEnums() + + +}//end class diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc index 9506a35c6e..eb1ca72058 100644 --- a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc @@ -28,6 +28,7 @@ class ContextSensitiveKeywords const /* testEndIf */ ENDIF = 'ENDIF'; const /* testEndSwitch */ ENDSWITCH = 'ENDSWITCH'; const /* testEndWhile */ ENDWHILE = 'ENDWHILE'; + const /* testEnum */ ENUM = 'ENUM'; const /* testExit */ EXIT = 'EXIT'; const /* testExtends */ EXTENDS = 'EXTENDS'; const /* testFinal */ FINAL = 'FINAL'; @@ -113,6 +114,7 @@ namespace /* testNamespaceNameIsString1 */ my\ /* testNamespaceNameIsString2 */ /* testInterfaceIsKeyword */ interface SomeInterface {} /* testTraitIsKeyword */ trait SomeTrait {} +/* testEnumIsKeyword */ enum SomeEnum {} $object = /* testNewIsKeyword */ new SomeClass(); $object /* testInstanceOfIsKeyword */ instanceof SomeClass; diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php index 4c200fbc30..72aeac6859 100644 --- a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php @@ -71,6 +71,7 @@ public function dataStrings() ['/* testEndIf */'], ['/* testEndSwitch */'], ['/* testEndWhile */'], + ['/* testEnum */'], ['/* testExit */'], ['/* testExtends */'], ['/* testFinal */'], @@ -251,6 +252,10 @@ public function dataKeywords() '/* testTraitIsKeyword */', 'T_TRAIT', ], + [ + '/* testEnumIsKeyword */', + 'T_ENUM', + ], [ '/* testNewIsKeyword */',