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