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 */',