From 53dd20ca94c44a4614add3cea56c2cfe76626ab0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Feb 2024 02:52:09 +0100 Subject: [PATCH] PHP 8.3 | Tokenizer/PHP: add support for typed OO constants PHP 8.3 introduced typed OO constants, where the type is between the `const` keyword and the constant name. All type variations are supported, including nullable types, union types, intersection types, with the exception of `callable`, `void` and `never`. `self` and `static` types are only allowed in Enum constants. This PR adds support for typed OO constants in the Tokenizer layer of PHPCS. The following issues had to be fixed to support typed constants: 1. Consistently tokenizing the constant _name_ as `T_STRING`, even if the name mirrors a reserved keyword, like `foreach` or a special keyword, like `self` or `true`. 2. Tokenizing a `?` at the start of a constant type declaration as `T_NULLABLE`. 3. Tokenizing a `|` and `&` operators within a constant type declaration as `T_TYPE_UNION` and `T_TYPE_INTERSECTION` respectively. Each and every part of the above has been covered by extensive tests. Includes additional tests safeguarding that the `array` keyword when used in a type declaration for a constant is tokenized as `T_STRING`. Ref: https://wiki.php.net/rfc/typed_class_constants --- src/Tokenizers/PHP.php | 102 +++- tests/Core/Tokenizer/ArrayKeywordTest.inc | 10 +- tests/Core/Tokenizer/ArrayKeywordTest.php | 17 +- tests/Core/Tokenizer/BitwiseOrTest.inc | 24 + tests/Core/Tokenizer/BitwiseOrTest.php | 9 + .../ContextSensitiveKeywordsTest.inc | 6 + .../ContextSensitiveKeywordsTest.php | 19 + .../OtherContextSensitiveKeywordsTest.inc | 14 + .../OtherContextSensitiveKeywordsTest.php | 90 +++ tests/Core/Tokenizer/TypeIntersectionTest.inc | 24 + tests/Core/Tokenizer/TypeIntersectionTest.php | 9 + tests/Core/Tokenizer/TypedConstantsTest.inc | 132 +++++ tests/Core/Tokenizer/TypedConstantsTest.php | 515 ++++++++++++++++++ 13 files changed, 963 insertions(+), 8 deletions(-) create mode 100644 tests/Core/Tokenizer/TypedConstantsTest.inc create mode 100644 tests/Core/Tokenizer/TypedConstantsTest.php diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index ecee80c430..31cc0237a2 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -526,8 +526,9 @@ protected function tokenize($string) $numTokens = count($tokens); $lastNotEmptyToken = 0; - $insideInlineIf = []; - $insideUseGroup = false; + $insideInlineIf = []; + $insideUseGroup = false; + $insideConstDeclaration = false; $commentTokenizer = new Comment(); @@ -608,7 +609,8 @@ protected function tokenize($string) if ($tokenIsArray === true && isset(Util\Tokens::$contextSensitiveKeywords[$token[0]]) === true && (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true - || $finalTokens[$lastNotEmptyToken]['content'] === '&') + || $finalTokens[$lastNotEmptyToken]['content'] === '&' + || $insideConstDeclaration === true) ) { if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) { $preserveKeyword = false; @@ -665,6 +667,30 @@ protected function tokenize($string) } }//end if + // Types in typed constants should not be touched, but the constant name should be. + if ((isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true + && $finalTokens[$lastNotEmptyToken]['code'] === T_CONST) + || $insideConstDeclaration === true + ) { + $preserveKeyword = true; + + // Find the next non-empty token. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === true + && isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true + ) { + continue; + } + + break; + } + + if ($tokens[$i] === '=' || $tokens[$i] === ';') { + $preserveKeyword = false; + $insideConstDeclaration = false; + } + }//end if + if ($finalTokens[$lastNotEmptyToken]['content'] === '&') { $preserveKeyword = true; @@ -698,6 +724,26 @@ protected function tokenize($string) } }//end if + /* + Mark the start of a constant declaration to allow for handling keyword to T_STRING + convertion for constant names using reserved keywords. + */ + + if ($tokenIsArray === true && $token[0] === T_CONST) { + $insideConstDeclaration = true; + } + + /* + Close an open "inside constant declaration" marker when no keyword convertion was needed. + */ + + if ($insideConstDeclaration === true + && $tokenIsArray === false + && ($token[0] === '=' || $token[0] === ';') + ) { + $insideConstDeclaration = false; + } + /* Special case for `static` used as a function name, i.e. `static()`. */ @@ -1869,6 +1915,20 @@ protected function tokenize($string) $newToken = []; $newToken['content'] = '?'; + // For typed constants, we only need to check the token before the ? to be sure. + if ($finalTokens[$lastNotEmptyToken]['code'] === T_CONST) { + $newToken['code'] = T_NULLABLE; + $newToken['type'] = 'T_NULLABLE'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from ? to T_NULLABLE".PHP_EOL; + } + + $finalTokens[$newStackPtr] = $newToken; + $newStackPtr++; + continue; + } + /* * Check if the next non-empty token is one of the tokens which can be used * in type declarations. If not, it's definitely a ternary. @@ -2236,7 +2296,30 @@ function return types. We want to keep the parenthesis map clean, if ($tokenIsArray === true && $token[0] === T_STRING) { $preserveTstring = false; - if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) { + // True/false/parent/self/static in typed constants should be fixed to their own token, + // but the constant name should not be. + if ((isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true + && $finalTokens[$lastNotEmptyToken]['code'] === T_CONST) + || $insideConstDeclaration === true + ) { + // Find the next non-empty token. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === true + && isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true + ) { + continue; + } + + break; + } + + if ($tokens[$i] === '=') { + $preserveTstring = true; + $insideConstDeclaration = false; + } + } else if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true + && $finalTokens[$lastNotEmptyToken]['code'] !== T_CONST + ) { $preserveTstring = true; // Special case for syntax like: return new self/new parent @@ -3008,6 +3091,12 @@ protected function processAdditional() $suspectedType = 'return'; } + if ($this->tokens[$x]['code'] === T_EQUAL) { + // Possible constant declaration, the `T_STRING` name will have been skipped over already. + $suspectedType = 'constant'; + break; + } + break; }//end for @@ -3049,6 +3138,11 @@ protected function processAdditional() break; } + if ($suspectedType === 'constant' && $this->tokens[$x]['code'] === T_CONST) { + $confirmed = true; + break; + } + if ($suspectedType === 'property or parameter' && (isset(Util\Tokens::$scopeModifiers[$this->tokens[$x]['code']]) === true || $this->tokens[$x]['code'] === T_VAR diff --git a/tests/Core/Tokenizer/ArrayKeywordTest.inc b/tests/Core/Tokenizer/ArrayKeywordTest.inc index ce5c553cf6..ce211bda2a 100644 --- a/tests/Core/Tokenizer/ArrayKeywordTest.inc +++ b/tests/Core/Tokenizer/ArrayKeywordTest.inc @@ -21,10 +21,10 @@ $var = array( ); /* testFunctionDeclarationParamType */ -function foo(array $a) {} +function typedParam(array $a) {} /* testFunctionDeclarationReturnType */ -function foo($a) : int|array|null {} +function returnType($a) : int|array|null {} class Bar { /* testClassConst */ @@ -32,4 +32,10 @@ class Bar { /* testClassMethod */ public function array() {} + + /* testOOConstType */ + const array /* testTypedOOConstName */ ARRAY = /* testOOConstDefault */ array(); + + /* testOOPropertyType */ + protected array $property; } diff --git a/tests/Core/Tokenizer/ArrayKeywordTest.php b/tests/Core/Tokenizer/ArrayKeywordTest.php index 4e2a04a700..f81706c330 100644 --- a/tests/Core/Tokenizer/ArrayKeywordTest.php +++ b/tests/Core/Tokenizer/ArrayKeywordTest.php @@ -68,6 +68,9 @@ public static function dataArrayKeyword() 'nested: inner array' => [ 'testMarker' => '/* testNestedArray */', ], + 'OO constant default value' => [ + 'testMarker' => '/* testOOConstDefault */', + ], ]; }//end dataArrayKeyword() @@ -122,6 +125,12 @@ public static function dataArrayType() 'function union return type' => [ 'testMarker' => '/* testFunctionDeclarationReturnType */', ], + 'OO constant type' => [ + 'testMarker' => '/* testOOConstType */', + ], + 'OO property type' => [ + 'testMarker' => '/* testOOPropertyType */', + ], ]; }//end dataArrayType() @@ -167,13 +176,17 @@ public function testNotArrayKeyword($testMarker, $testContent='array') public static function dataNotArrayKeyword() { return [ - 'class-constant-name' => [ + 'class-constant-name' => [ 'testMarker' => '/* testClassConst */', 'testContent' => 'ARRAY', ], - 'class-method-name' => [ + 'class-method-name' => [ 'testMarker' => '/* testClassMethod */', ], + 'class-constant-name-after-type' => [ + 'testMarker' => '/* testTypedOOConstName */', + 'testContent' => 'ARRAY', + ], ]; }//end dataNotArrayKeyword() diff --git a/tests/Core/Tokenizer/BitwiseOrTest.inc b/tests/Core/Tokenizer/BitwiseOrTest.inc index 4667ad025b..5afc1e5bdf 100644 --- a/tests/Core/Tokenizer/BitwiseOrTest.inc +++ b/tests/Core/Tokenizer/BitwiseOrTest.inc @@ -9,6 +9,30 @@ $result = $value | $test /* testBitwiseOr2 */ | $another; class TypeUnion { + /* testTypeUnionOOConstSimple */ + public const Foo|Bar SIMPLE = new Foo; + + /* testTypeUnionOOConstReverseModifierOrder */ + protected final const int|float MODIFIERS_REVERSED /* testBitwiseOrOOConstDefaultValue */ = E_WARNING | E_NOTICE; + + const + /* testTypeUnionOOConstMulti1 */ + array | + /* testTypeUnionOOConstMulti2 */ + Traversable | // phpcs:ignore Stnd.Cat.Sniff + false + /* testTypeUnionOOConstMulti3 */ + | null MULTI_UNION = false; + + /* testTypeUnionOOConstNamespaceRelative */ + final protected const namespace\Sub\NameA|namespace\Sub\NameB NAMESPACE_RELATIVE = new namespace\Sub\NameB; + + /* testTypeUnionOOConstPartiallyQualified */ + const Partially\Qualified\NameA|Partially\Qualified\NameB PARTIALLY_QUALIFIED = new Partially\Qualified\NameA; + + /* testTypeUnionOOConstFullyQualified */ + const \Fully\Qualified\NameA|\Fully\Qualified\NameB FULLY_QUALIFIED = new \Fully\Qualified\NameB(); + /* testTypeUnionPropertySimple */ public static Foo|Bar $obj; diff --git a/tests/Core/Tokenizer/BitwiseOrTest.php b/tests/Core/Tokenizer/BitwiseOrTest.php index 71a1c51ccb..8e3e264f5b 100644 --- a/tests/Core/Tokenizer/BitwiseOrTest.php +++ b/tests/Core/Tokenizer/BitwiseOrTest.php @@ -47,6 +47,7 @@ public static function dataBitwiseOr() return [ 'in simple assignment 1' => ['/* testBitwiseOr1 */'], 'in simple assignment 2' => ['/* testBitwiseOr2 */'], + 'in OO constant default value' => ['/* testBitwiseOrOOConstDefaultValue */'], 'in property default value' => ['/* testBitwiseOrPropertyDefaultValue */'], 'in method parameter default value' => ['/* testBitwiseOrParamDefaultValue */'], 'in return statement' => ['/* testBitwiseOr3 */'], @@ -97,6 +98,14 @@ public function testTypeUnion($testMarker) public static function dataTypeUnion() { return [ + 'type for OO constant' => ['/* testTypeUnionOOConstSimple */'], + 'type for OO constant, reversed modifier order' => ['/* testTypeUnionOOConstReverseModifierOrder */'], + 'type for OO constant, first of multi-union' => ['/* testTypeUnionOOConstMulti1 */'], + 'type for OO constant, middle of multi-union + comments' => ['/* testTypeUnionOOConstMulti2 */'], + 'type for OO constant, last of multi-union' => ['/* testTypeUnionOOConstMulti3 */'], + 'type for OO constant, using namespace relative names' => ['/* testTypeUnionOOConstNamespaceRelative */'], + 'type for OO constant, using partially qualified names' => ['/* testTypeUnionOOConstPartiallyQualified */'], + 'type for OO constant, using fully qualified names' => ['/* testTypeUnionOOConstFullyQualified */'], 'type for static property' => ['/* testTypeUnionPropertySimple */'], 'type for static property, reversed modifier order' => ['/* testTypeUnionPropertyReverseModifierOrder */'], 'type for property, first of multi-union' => ['/* testTypeUnionPropertyMulti1 */'], diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc index abbfa62d4d..2d471285c2 100644 --- a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc @@ -76,6 +76,12 @@ class ContextSensitiveKeywords const /* testAnd */ AND = 'LOGICAL_AND'; const /* testOr */ OR = 'LOGICAL_OR'; const /* testXor */ XOR = 'LOGICAL_XOR'; + + const /* testArrayIsTstringInConstType */ array /* testArrayNameForTypedConstant */ ARRAY = /* testArrayIsKeywordInConstDefault */ array(); + const /* testStaticIsKeywordAsConstType */ static /* testStaticIsNameForTypedConstant */ STATIC = new /* testStaticIsKeywordAsConstDefault */ static; + + const int|bool /* testPrivateNameForUnionTypedConstant */ PRIVATE = 'PRIVATE'; + const Foo&Bar /* testFinalNameForIntersectionTypedConstant */ FINAL = 'FINAL'; } namespace /* testKeywordAfterNamespaceShouldBeString */ class; diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php index 0c311a73bb..51c5453873 100644 --- a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php @@ -118,6 +118,12 @@ public static function dataStrings() 'constant declaration: or' => ['/* testOr */'], 'constant declaration: xor' => ['/* testXor */'], + 'constant declaration: array in type' => ['/* testArrayIsTstringInConstType */'], + 'constant declaration: array, name after type' => ['/* testArrayNameForTypedConstant */'], + 'constant declaration: static, name after type' => ['/* testStaticIsNameForTypedConstant */'], + 'constant declaration: private, name after type' => ['/* testPrivateNameForUnionTypedConstant */'], + 'constant declaration: final, name after type' => ['/* testFinalNameForIntersectionTypedConstant */'], + 'namespace declaration: class' => ['/* testKeywordAfterNamespaceShouldBeString */'], 'namespace declaration (partial): my' => ['/* testNamespaceNameIsString1 */'], 'namespace declaration (partial): class' => ['/* testNamespaceNameIsString2 */'], @@ -179,6 +185,19 @@ public static function dataKeywords() 'testMarker' => '/* testNamespaceIsKeyword */', 'expectedTokenType' => 'T_NAMESPACE', ], + 'array: default value in const decl' => [ + 'testMarker' => '/* testArrayIsKeywordInConstDefault */', + 'expectedTokenType' => 'T_ARRAY', + ], + 'static: type in constant declaration' => [ + 'testMarker' => '/* testStaticIsKeywordAsConstType */', + 'expectedTokenType' => 'T_STATIC', + ], + 'static: value in constant declaration' => [ + 'testMarker' => '/* testStaticIsKeywordAsConstDefault */', + 'expectedTokenType' => 'T_STATIC', + ], + 'abstract: class declaration' => [ 'testMarker' => '/* testAbstractIsKeyword */', 'expectedTokenType' => 'T_ABSTRACT', diff --git a/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc index f19992c150..50b2c2ee24 100644 --- a/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc +++ b/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc @@ -51,3 +51,17 @@ function standAloneFalseTrueNullTypesAndMore( || $a === /* testNullIsKeywordInComparison */ null ) {} } + +class TypedConstProp { + const /* testFalseIsKeywordAsConstType */ false /* testFalseIsNameForTypedConstant */ FALSE = /* testFalseIsKeywordAsConstDefault */ false; + const /* testTrueIsKeywordAsConstType */ true /* testTrueIsNameForTypedConstant */ TRUE = /* testTrueIsKeywordAsConstDefault */ true; + const /* testNullIsKeywordAsConstType */ null /* testNullIsNameForTypedConstant */ NULL = /* testNullIsKeywordAsConstDefault */ null; + const /* testSelfIsKeywordAsConstType */ self /* testSelfIsNameForTypedConstant */ SELF = new /* testSelfIsKeywordAsConstDefault */ self; + const /* testParentIsKeywordAsConstType */ parent /* testParentIsNameForTypedConstant */ PARENT = new /* testParentIsKeywordAsConstDefault */ parent; + + public /* testFalseIsKeywordAsPropertyType */ false $false = /* testFalseIsKeywordAsPropertyDefault */ false; + protected readonly /* testTrueIsKeywordAsPropertyType */ true $true = /* testTrueIsKeywordAsPropertyDefault */ true; + static private /* testNullIsKeywordAsPropertyType */ null $null = /* testNullIsKeywordAsPropertyDefault */ null; + var /* testSelfIsKeywordAsPropertyType */ self $self = new /* testSelfIsKeywordAsPropertyDefault */ self; + protected /* testParentIsKeywordAsPropertyType */ parent $parent = new /* testParentIsKeywordAsPropertyDefault */ parent; +} diff --git a/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.php index cb55d5ca7e..7473cdf3c9 100644 --- a/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.php @@ -86,6 +86,12 @@ public static function dataStrings() 'class instantiation: false' => ['/* testClassInstantiationFalseIsString */'], 'class instantiation: true' => ['/* testClassInstantiationTrueIsString */'], 'class instantiation: null' => ['/* testClassInstantiationNullIsString */'], + + 'constant declaration: false as name after type' => ['/* testFalseIsNameForTypedConstant */'], + 'constant declaration: true as name after type' => ['/* testTrueIsNameForTypedConstant */'], + 'constant declaration: null as name after type' => ['/* testNullIsNameForTypedConstant */'], + 'constant declaration: self as name after type' => ['/* testSelfIsNameForTypedConstant */'], + 'constant declaration: parent as name after type' => ['/* testParentIsNameForTypedConstant */'], ]; }//end dataStrings() @@ -185,6 +191,90 @@ public static function dataKeywords() 'testMarker' => '/* testNullIsKeywordInComparison */', 'expectedTokenType' => 'T_NULL', ], + + 'false: type in OO constant declaration' => [ + 'testMarker' => '/* testFalseIsKeywordAsConstType */', + 'expectedTokenType' => 'T_FALSE', + ], + 'true: type in OO constant declaration' => [ + 'testMarker' => '/* testTrueIsKeywordAsConstType */', + 'expectedTokenType' => 'T_TRUE', + ], + 'null: type in OO constant declaration' => [ + 'testMarker' => '/* testNullIsKeywordAsConstType */', + 'expectedTokenType' => 'T_NULL', + ], + 'self: type in OO constant declaration' => [ + 'testMarker' => '/* testSelfIsKeywordAsConstType */', + 'expectedTokenType' => 'T_SELF', + ], + 'parent: type in OO constant declaration' => [ + 'testMarker' => '/* testParentIsKeywordAsConstType */', + 'expectedTokenType' => 'T_PARENT', + ], + + 'false: value in constant declaration' => [ + 'testMarker' => '/* testFalseIsKeywordAsConstDefault */', + 'expectedTokenType' => 'T_FALSE', + ], + 'true: value in constant declaration' => [ + 'testMarker' => '/* testTrueIsKeywordAsConstDefault */', + 'expectedTokenType' => 'T_TRUE', + ], + 'null: value in constant declaration' => [ + 'testMarker' => '/* testNullIsKeywordAsConstDefault */', + 'expectedTokenType' => 'T_NULL', + ], + 'self: value in constant declaration' => [ + 'testMarker' => '/* testSelfIsKeywordAsConstDefault */', + 'expectedTokenType' => 'T_SELF', + ], + 'parent: value in constant declaration' => [ + 'testMarker' => '/* testParentIsKeywordAsConstDefault */', + 'expectedTokenType' => 'T_PARENT', + ], + + 'false: type in property declaration' => [ + 'testMarker' => '/* testFalseIsKeywordAsPropertyType */', + 'expectedTokenType' => 'T_FALSE', + ], + 'true: type in property declaration' => [ + 'testMarker' => '/* testTrueIsKeywordAsPropertyType */', + 'expectedTokenType' => 'T_TRUE', + ], + 'null: type in property declaration' => [ + 'testMarker' => '/* testNullIsKeywordAsPropertyType */', + 'expectedTokenType' => 'T_NULL', + ], + 'self: type in property declaration' => [ + 'testMarker' => '/* testSelfIsKeywordAsPropertyType */', + 'expectedTokenType' => 'T_SELF', + ], + 'parent: type in property declaration' => [ + 'testMarker' => '/* testParentIsKeywordAsPropertyType */', + 'expectedTokenType' => 'T_PARENT', + ], + + 'false: value in property declaration' => [ + 'testMarker' => '/* testFalseIsKeywordAsPropertyDefault */', + 'expectedTokenType' => 'T_FALSE', + ], + 'true: value in property declaration' => [ + 'testMarker' => '/* testTrueIsKeywordAsPropertyDefault */', + 'expectedTokenType' => 'T_TRUE', + ], + 'null: value in property declaration' => [ + 'testMarker' => '/* testNullIsKeywordAsPropertyDefault */', + 'expectedTokenType' => 'T_NULL', + ], + 'self: value in property declaration' => [ + 'testMarker' => '/* testSelfIsKeywordAsPropertyDefault */', + 'expectedTokenType' => 'T_SELF', + ], + 'parent: value in property declaration' => [ + 'testMarker' => '/* testParentIsKeywordAsPropertyDefault */', + 'expectedTokenType' => 'T_PARENT', + ], ]; }//end dataKeywords() diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.inc b/tests/Core/Tokenizer/TypeIntersectionTest.inc index 10cd56b2db..d9a68db363 100644 --- a/tests/Core/Tokenizer/TypeIntersectionTest.inc +++ b/tests/Core/Tokenizer/TypeIntersectionTest.inc @@ -9,6 +9,30 @@ $result = $value & $test /* testBitwiseAnd2 */ & $another; class TypeIntersection { + /* testTypeIntersectionOOConstSimple */ + public const Foo&Bar SIMPLE = new Foo(); + + /* testTypeIntersectionOOConstReverseModifierOrder */ + protected final const Something&Nothing MODIFIERS_REVERSED /* testBitwiseAndOOConstDefaultValue */ = E_WARNING & E_NOTICE; + + const + /* testTypeIntersectionOOConstMulti1 */ + Foo & + /* testTypeIntersectionOOConstMulti2 */ + Traversable & // phpcs:ignore Stnd.Cat.Sniff + Boo + /* testTypeIntersectionOOConstMulti3 */ + & Bar MULTI_INTERSECTION = false; + + /* testTypeIntersectionOOConstNamespaceRelative */ + const namespace\Sub\NameA&namespace\Sub\NameB NAMESPACE_RELATIVE = new namespace\Sub\NameB; + + /* testTypeIntersectionOOConstPartiallyQualified */ + const Partially\Qualified\NameA&Partially\Qualified\NameB PARTIALLY_QUALIFIED = new Partially\Qualified\NameA; + + /* testTypeIntersectionOOConstFullyQualified */ + const \Fully\Qualified\NameA&\Fully\Qualified\NameB FULLY_QUALIFIED = new \Fully\Qualified\NameB(); + /* testTypeIntersectionPropertySimple */ public static Foo&Bar $obj; diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.php b/tests/Core/Tokenizer/TypeIntersectionTest.php index 32710f7fef..c719850c19 100644 --- a/tests/Core/Tokenizer/TypeIntersectionTest.php +++ b/tests/Core/Tokenizer/TypeIntersectionTest.php @@ -48,6 +48,7 @@ public static function dataBitwiseAnd() return [ 'in simple assignment 1' => ['/* testBitwiseAnd1 */'], 'in simple assignment 2' => ['/* testBitwiseAnd2 */'], + 'in OO constant default value' => ['/* testBitwiseAndOOConstDefaultValue */'], 'in property default value' => ['/* testBitwiseAndPropertyDefaultValue */'], 'in method parameter default value' => ['/* testBitwiseAndParamDefaultValue */'], 'reference for method parameter' => ['/* testBitwiseAnd3 */'], @@ -100,6 +101,14 @@ public function testTypeIntersection($testMarker) public static function dataTypeIntersection() { return [ + 'type for OO constant' => ['/* testTypeIntersectionOOConstSimple */'], + 'type for OO constant, reversed modifier order' => ['/* testTypeIntersectionOOConstReverseModifierOrder */'], + 'type for OO constant, first of multi-intersect' => ['/* testTypeIntersectionOOConstMulti1 */'], + 'type for OO constant, middle of multi-intersect + comments' => ['/* testTypeIntersectionOOConstMulti2 */'], + 'type for OO constant, last of multi-intersect' => ['/* testTypeIntersectionOOConstMulti3 */'], + 'type for OO constant, using namespace relative names' => ['/* testTypeIntersectionOOConstNamespaceRelative */'], + 'type for OO constant, using partially qualified names' => ['/* testTypeIntersectionOOConstPartiallyQualified */'], + 'type for OO constant, using fully qualified names' => ['/* testTypeIntersectionOOConstFullyQualified */'], 'type for static property' => ['/* testTypeIntersectionPropertySimple */'], 'type for static property, reversed modifier order' => ['/* testTypeIntersectionPropertyReverseModifierOrder */'], 'type for property, first of multi-intersect' => ['/* testTypeIntersectionPropertyMulti1 */'], diff --git a/tests/Core/Tokenizer/TypedConstantsTest.inc b/tests/Core/Tokenizer/TypedConstantsTest.inc new file mode 100644 index 0000000000..a68831392c --- /dev/null +++ b/tests/Core/Tokenizer/TypedConstantsTest.inc @@ -0,0 +1,132 @@ +const ? 0 : 1; + +/* testGlobalConstantCannotBeTyped */ +const GLOBAL_UNTYPED = true; + +/* testGlobalConstantTypedShouldStillBeHandled */ +const ?int GLOBAL_TYPED = true; + +class ClassWithPlainTypedConstants { + /* testClassConstFinalUntyped */ + final const FINAL_UNTYPED = true; + + /* testClassConstVisibilityUntyped */ + public const /*comment*/VISIBLE_UNTYPED = true; + + /* testClassConstTypedTrue */ + const true TYPED_TRUE = true; + /* testClassConstTypedFalse */ + final const false TYPED_FALSE = false; + /* testClassConstTypedNull */ + public const null TYPED_NULL = null; + /* testClassConstTypedBool */ + final protected const/*comment*/bool TYPED_BOOL = false; + /* testClassConstTypedInt */ + private const int TYPED_INT = 0; + /* testClassConstTypedFloat */ + const float TYPED_FLOAT = 0.5; + /* testClassConstTypedString */ + public final const string/*comment*/TYPED_STRING = 'string'; + /* testClassConstTypedArray */ + private final const array TYPED_ARRAY = []; + /* testClassConstTypedObject */ + const + object + TYPED_OBJECT = MyClass::getInstance(); + /* testClassConstTypedIterable */ + const iterable typed_iterable = []; + /* testClassConstTypedMixed */ + const mixed TYPED_MIXED = 'string'; + + /* testClassConstTypedClassUnqualified */ + const MyClass TYPED_UNQUALIFIED_CLASSNAME = MyClass::getInstance(); + /* testClassConstTypedClassFullyQualified */ + public const \MyClass TYPED_FULLY_QUALIFIED_CLASSNAME = MyClass::getInstance(); + /* testClassConstTypedClassNamespaceRelative */ + protected const namespace\MyClass TYPED_NAMESPACE_RELATIVE_CLASSNAME = MyClass::getInstance(); + /* testClassConstTypedClassPartiallyQualified */ + private const Partial\MyClass TYPED_PARTIALLY_QUALIFIED_CLASSNAME = MyClass::getInstance(); + /* testClassConstTypedParent */ + const parent TYPED_PARENT = parent::getInstance(); + + // Illegal types - the fact that these are not allowed in PHP is not the concern of the PHPCS tokenizer. + /* testClassConstTypedCallable */ + protected const callable TYPED_CALLABLE = 'function_name'; + /* testClassConstTypedVoid */ + protected const void TYPED_VOID = null; + /* testClassConstTypedNever */ + protected const NEVER TYPED_NEVER = null; +} + +trait TraitWithNullableTypedConstants { + /* testTraitConstTypedNullableTrue */ + const ?true TYPED_TRUE = true; + /* testTraitConstTypedNullableFalse */ + final const ?false TYPED_FALSE = false; + /* testTraitConstTypedNullableNull */ + public const ?null TYPED_NULL = null; + /* testTraitConstTypedNullableBool */ + final protected const ?bool TYPED_BOOL = false; + /* testTraitConstTypedNullableInt */ + private const ?int TYPED_INT = 0; + /* testTraitConstTypedNullableFloat */ + const ? /*comment*/ float TYPED_FLOAT = 0.5; + /* testTraitConstTypedNullableString */ + public final const ?string TYPED_STRING = 'string'; + /* testTraitConstTypedNullableArray */ + private final const ? array TYPED_ARRAY = []; + /* testTraitConstTypedNullableObject */ + const ?object TYPED_OBJECT = MyClass::getInstance(); + /* testTraitConstTypedNullableIterable */ + const ?iterable TYPED_ITERABLE = []; + /* testTraitConstTypedNullableMixed */ + const ?mixed TYPED_MIXED = 'string'; + + /* testTraitConstTypedNullableClassUnqualified */ + const ?MyClass TYPED_UNQUALIFIED_CLASSNAME = MyClass::getInstance(); + /* testTraitConstTypedNullableClassFullyQualified */ + public const ?\MyClass TYPED_FULLY_QUALIFIED_CLASSNAME = MyClass::getInstance(); + /* testTraitConstTypedNullableClassNamespaceRelative */ + protected const ?namespace\MyClass TYPED_NAMESPACE_RELATIVE_CLASSNAME = MyClass::getInstance(); + /* testTraitConstTypedNullableClassPartiallyQualified */ + private const ?Partial\MyClass TYPED_PARTIALLY_QUALIFIED_CLASSNAME = MyClass::getInstance(); + /* testTraitConstTypedNullableParent */ + const ?parent TYPED_PARENT = parent::getInstance(); +} + +interface InterfaceWithUnionTypedConstants { + /* testInterfaceConstTypedUnionTrueNull */ + const true|null /*comment*/ UNION_TRUE_NULL = true; + /* testInterfaceConstTypedUnionArrayObject */ + const array|object UNION_ARRAY_OBJECT = []; + /* testInterfaceConstTypedUnionStringArrayInt */ + const string | array | int UNION_STRING_ARRAY_INT = 'array middle'; + /* testInterfaceConstTypedUnionFloatBoolArray */ + const float /*comment*/| bool|array UNION_FLOAT_BOOL_ARRAY = false; + /* testInterfaceConstTypedUnionIterableFalse */ + const iterable|false UNION_ITERABLE_FALSE = false; + /* testInterfaceConstTypedUnionUnqualifiedNamespaceRelative */ + const Unqualified|namespace\Relative UNION_UNQUALIFIED_NSRELATIVE = new Unqualified(); + /* testInterfaceConstTypedUnionFullyQualifiedPartiallyQualified */ + const \Fully\Qualified|Partially\Qualified UNION_FQN_PARTIAL = new Partial\Qualified; +} + +enum EnumWithIntersectionTypedConstants { + // Illegal types in a class, but legal in an enum. + /* testEnumConstTypedSelf */ + final const self TYPED_SELF = self::getInstance(); + /* testEnumConstTypedStatic */ + const static TYPED_STATIC = static::getInstance(); + /* testEnumConstTypedNullableSelf */ + public const ?self TYPED_SELF = self::getInstance(); + /* testEnumConstTypedNullableStatic */ + const ?static TYPED_STATIC = static::getInstance(); + + /* testEnumConstTypedIntersectUnqualifiedNamespaceRelative */ + const Unqualified&namespace\Relative UNION_UNQUALIFIED_NSRELATIVE = new Unqualified(); + /* testEnumConstTypedIntersectFullyQualifiedPartiallyQualified */ + const \Fully\Qualified&Partially\Qualified UNION_FQN_PARTIAL = new Partial\Qualified; +} diff --git a/tests/Core/Tokenizer/TypedConstantsTest.php b/tests/Core/Tokenizer/TypedConstantsTest.php new file mode 100644 index 0000000000..0c4a9b6146 --- /dev/null +++ b/tests/Core/Tokenizer/TypedConstantsTest.php @@ -0,0 +1,515 @@ + + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Util\Tokens; + +final class TypedConstantsTest extends AbstractTokenizerTestCase +{ + + + /** + * Test that a ? after a "const" which is not the constant keyword is tokenized as ternary then, not as the nullable operator. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testTernaryIsInlineThen() + { + $tokens = $this->phpcsFile->getTokens(); + $target = $this->getTargetToken('/* testTernaryIsTernaryAfterConst */', [T_NULLABLE, T_INLINE_THEN]); + + $this->assertSame( + T_INLINE_THEN, + $tokens[$target]['code'], + 'Token tokenized as '.Tokens::tokenName($tokens[$target]['code']).', not T_INLINE_THEN (code)' + ); + $this->assertSame( + 'T_INLINE_THEN', + $tokens[$target]['type'], + 'Token tokenized as '.$tokens[$target]['type'].', not T_INLINE_THEN (type)' + ); + + }//end testTernaryIsInlineThen() + + + /** + * Test the token name for an untyped constant is tokenized as T_STRING. + * + * @param string $testMarker The comment prefacing the target token. + * + * @dataProvider dataUntypedConstant + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testUntypedConstant($testMarker) + { + $tokens = $this->phpcsFile->getTokens(); + $target = $this->getTargetToken($testMarker, T_CONST); + + for ($i = ($target + 1); $tokens[$i]['code'] !== T_EQUAL; $i++) { + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { + // Ignore whitespace and comments, not interested in the tokenization of those. + continue; + } + + $this->assertSame( + T_STRING, + $tokens[$i]['code'], + 'Token tokenized as '.Tokens::tokenName($tokens[$i]['code']).', not T_STRING (code)' + ); + $this->assertSame( + 'T_STRING', + $tokens[$i]['type'], + 'Token tokenized as '.$tokens[$i]['type'].', not T_STRING (type)' + ); + } + + }//end testUntypedConstant() + + + /** + * Data provider. + * + * @see testUntypedConstant() + * + * @return array> + */ + public static function dataUntypedConstant() + { + return [ + 'non OO constant (untyped)' => [ + 'testMarker' => '/* testGlobalConstantCannotBeTyped */', + ], + 'OO constant, final, untyped' => [ + 'testMarker' => '/* testClassConstFinalUntyped */', + ], + 'OO constant, public, untyped, with comment' => [ + 'testMarker' => '/* testClassConstVisibilityUntyped */', + ], + ]; + + }//end dataUntypedConstant() + + + /** + * Test the tokens in the type of a typed constant as well as the constant name are tokenized correctly. + * + * @param string $testMarker The comment prefacing the target token. + * @param string $sequence The expected token sequence. + * + * @dataProvider dataTypedConstant + * @dataProvider dataNullableTypedConstant + * @dataProvider dataUnionTypedConstant + * @dataProvider dataIntersectionTypedConstant + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testTypedConstant($testMarker, array $sequence) + { + $tokens = $this->phpcsFile->getTokens(); + $target = $this->getTargetToken($testMarker, T_CONST); + + $current = 0; + for ($i = ($target + 1); $tokens[$i]['code'] !== T_EQUAL; $i++) { + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { + // Ignore whitespace and comments, not interested in the tokenization of those. + continue; + } + + $this->assertSame( + $sequence[$current], + $tokens[$i]['code'], + 'Token tokenized as '.Tokens::tokenName($tokens[$i]['code']).', not '.Tokens::tokenName($sequence[$current]).' (code)' + ); + + ++$current; + } + + }//end testTypedConstant() + + + /** + * Data provider. + * + * @see testTypedConstant() + * + * @return array> + */ + public static function dataTypedConstant() + { + $data = [ + 'simple type: true' => [ + 'testMarker' => '/* testClassConstTypedTrue */', + 'sequence' => [T_TRUE], + ], + 'simple type: false' => [ + 'testMarker' => '/* testClassConstTypedFalse */', + 'sequence' => [T_FALSE], + ], + 'simple type: null' => [ + 'testMarker' => '/* testClassConstTypedNull */', + 'sequence' => [T_NULL], + ], + 'simple type: bool' => [ + 'testMarker' => '/* testClassConstTypedBool */', + 'sequence' => [T_STRING], + ], + 'simple type: int' => [ + 'testMarker' => '/* testClassConstTypedInt */', + 'sequence' => [T_STRING], + ], + 'simple type: float' => [ + 'testMarker' => '/* testClassConstTypedFloat */', + 'sequence' => [T_STRING], + ], + 'simple type: string' => [ + 'testMarker' => '/* testClassConstTypedString */', + 'sequence' => [T_STRING], + ], + 'simple type: array' => [ + 'testMarker' => '/* testClassConstTypedArray */', + 'sequence' => [T_STRING], + ], + 'simple type: object' => [ + 'testMarker' => '/* testClassConstTypedObject */', + 'sequence' => [T_STRING], + ], + 'simple type: iterable' => [ + 'testMarker' => '/* testClassConstTypedIterable */', + 'sequence' => [T_STRING], + ], + 'simple type: mixed' => [ + 'testMarker' => '/* testClassConstTypedMixed */', + 'sequence' => [T_STRING], + ], + 'simple type: unqualified name' => [ + 'testMarker' => '/* testClassConstTypedClassUnqualified */', + 'sequence' => [T_STRING], + ], + 'simple type: fully qualified name' => [ + 'testMarker' => '/* testClassConstTypedClassFullyQualified */', + 'sequence' => [ + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'simple type: namespace relative name' => [ + 'testMarker' => '/* testClassConstTypedClassNamespaceRelative */', + 'sequence' => [ + T_NAMESPACE, + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'simple type: partially qualified name' => [ + 'testMarker' => '/* testClassConstTypedClassPartiallyQualified */', + 'sequence' => [ + T_STRING, + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'simple type: parent' => [ + 'testMarker' => '/* testClassConstTypedParent */', + 'sequence' => [T_PARENT], + ], + + 'simple type: callable (invalid)' => [ + 'testMarker' => '/* testClassConstTypedCallable */', + 'sequence' => [T_CALLABLE], + ], + 'simple type: void (invalid)' => [ + 'testMarker' => '/* testClassConstTypedVoid */', + 'sequence' => [T_STRING], + ], + 'simple type: NEVER (invalid)' => [ + 'testMarker' => '/* testClassConstTypedNever */', + 'sequence' => [T_STRING], + ], + + 'simple type: self (only valid in enum)' => [ + 'testMarker' => '/* testEnumConstTypedSelf */', + 'sequence' => [T_SELF], + ], + 'simple type: static (only valid in enum)' => [ + 'testMarker' => '/* testEnumConstTypedStatic */', + 'sequence' => [T_STATIC], + ], + ]; + + // The constant name, as the last token in the sequence, is always T_STRING. + foreach ($data as $key => $value) { + $data[$key]['sequence'][] = T_STRING; + } + + return $data; + + }//end dataTypedConstant() + + + /** + * Data provider. + * + * @see testTypedConstant() + * + * @return array> + */ + public static function dataNullableTypedConstant() + { + $data = [ + // Global constants cannot be typed in PHP, but that's not our concern. + 'global typed constant, invalid, ?int' => [ + 'testMarker' => '/* testGlobalConstantTypedShouldStillBeHandled */', + 'sequence' => [T_STRING], + ], + + // OO constants. + 'nullable type: true' => [ + 'testMarker' => '/* testTraitConstTypedNullableTrue */', + 'sequence' => [T_TRUE], + ], + 'nullable type: false' => [ + 'testMarker' => '/* testTraitConstTypedNullableFalse */', + 'sequence' => [T_FALSE], + ], + 'nullable type: null' => [ + 'testMarker' => '/* testTraitConstTypedNullableNull */', + 'sequence' => [T_NULL], + ], + 'nullable type: bool' => [ + 'testMarker' => '/* testTraitConstTypedNullableBool */', + 'sequence' => [T_STRING], + ], + 'nullable type: int' => [ + 'testMarker' => '/* testTraitConstTypedNullableInt */', + 'sequence' => [T_STRING], + ], + 'nullable type: float' => [ + 'testMarker' => '/* testTraitConstTypedNullableFloat */', + 'sequence' => [T_STRING], + ], + 'nullable type: string' => [ + 'testMarker' => '/* testTraitConstTypedNullableString */', + 'sequence' => [T_STRING], + ], + 'nullable type: array' => [ + 'testMarker' => '/* testTraitConstTypedNullableArray */', + 'sequence' => [T_STRING], + ], + 'nullable type: object' => [ + 'testMarker' => '/* testTraitConstTypedNullableObject */', + 'sequence' => [T_STRING], + ], + 'nullable type: iterable' => [ + 'testMarker' => '/* testTraitConstTypedNullableIterable */', + 'sequence' => [T_STRING], + ], + 'nullable type: mixed' => [ + 'testMarker' => '/* testTraitConstTypedNullableMixed */', + 'sequence' => [T_STRING], + ], + 'nullable type: unqualified name' => [ + 'testMarker' => '/* testTraitConstTypedNullableClassUnqualified */', + 'sequence' => [T_STRING], + ], + 'nullable type: fully qualified name' => [ + 'testMarker' => '/* testTraitConstTypedNullableClassFullyQualified */', + 'sequence' => [ + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'nullable type: namespace relative name' => [ + 'testMarker' => '/* testTraitConstTypedNullableClassNamespaceRelative */', + 'sequence' => [ + T_NAMESPACE, + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'nullable type: partially qualified name' => [ + 'testMarker' => '/* testTraitConstTypedNullableClassPartiallyQualified */', + 'sequence' => [ + T_STRING, + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'nullable type: parent' => [ + 'testMarker' => '/* testTraitConstTypedNullableParent */', + 'sequence' => [T_PARENT], + ], + + 'nullable type: self (only valid in enum)' => [ + 'testMarker' => '/* testEnumConstTypedNullableSelf */', + 'sequence' => [T_SELF], + ], + 'nullable type: static (only valid in enum)' => [ + 'testMarker' => '/* testEnumConstTypedNullableStatic */', + 'sequence' => [T_STATIC], + ], + ]; + + // The nullable operator, as the first token in the sequence, is always T_NULLABLE. + // The constant name, as the last token in the sequence, is always T_STRING. + foreach ($data as $key => $value) { + array_unshift($data[$key]['sequence'], T_NULLABLE); + $data[$key]['sequence'][] = T_STRING; + } + + return $data; + + }//end dataNullableTypedConstant() + + + /** + * Data provider. + * + * @see testTypedConstant() + * + * @return array> + */ + public static function dataUnionTypedConstant() + { + $data = [ + 'union type: true|null' => [ + 'testMarker' => '/* testInterfaceConstTypedUnionTrueNull */', + 'sequence' => [ + T_TRUE, + T_TYPE_UNION, + T_NULL, + ], + ], + 'union type: array|object' => [ + 'testMarker' => '/* testInterfaceConstTypedUnionArrayObject */', + 'sequence' => [ + T_STRING, + T_TYPE_UNION, + T_STRING, + ], + ], + 'union type: string|array|int' => [ + 'testMarker' => '/* testInterfaceConstTypedUnionStringArrayInt */', + 'sequence' => [ + T_STRING, + T_TYPE_UNION, + T_STRING, + T_TYPE_UNION, + T_STRING, + ], + ], + 'union type: float|bool|array' => [ + 'testMarker' => '/* testInterfaceConstTypedUnionFloatBoolArray */', + 'sequence' => [ + T_STRING, + T_TYPE_UNION, + T_STRING, + T_TYPE_UNION, + T_STRING, + ], + ], + 'union type: iterable|false' => [ + 'testMarker' => '/* testInterfaceConstTypedUnionIterableFalse */', + 'sequence' => [ + T_STRING, + T_TYPE_UNION, + T_FALSE, + ], + ], + 'union type: Unqualified|Namespace\Relative' => [ + 'testMarker' => '/* testInterfaceConstTypedUnionUnqualifiedNamespaceRelative */', + 'sequence' => [ + T_STRING, + T_TYPE_UNION, + T_NAMESPACE, + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'union type: FQN|Partial' => [ + 'testMarker' => '/* testInterfaceConstTypedUnionFullyQualifiedPartiallyQualified */', + 'sequence' => [ + T_NS_SEPARATOR, + T_STRING, + T_NS_SEPARATOR, + T_STRING, + T_TYPE_UNION, + T_STRING, + T_NS_SEPARATOR, + T_STRING, + ], + ], + ]; + + // The constant name, as the last token in the sequence, is always T_STRING. + foreach ($data as $key => $value) { + $data[$key]['sequence'][] = T_STRING; + } + + return $data; + + }//end dataUnionTypedConstant() + + + /** + * Data provider. + * + * @see testTypedConstant() + * + * @return array> + */ + public static function dataIntersectionTypedConstant() + { + $data = [ + 'intersection type: Unqualified&Namespace\Relative' => [ + 'testMarker' => '/* testEnumConstTypedIntersectUnqualifiedNamespaceRelative */', + 'sequence' => [ + T_STRING, + T_TYPE_INTERSECTION, + T_NAMESPACE, + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'intersection type: FQN&Partial' => [ + 'testMarker' => '/* testEnumConstTypedIntersectFullyQualifiedPartiallyQualified */', + 'sequence' => [ + T_NS_SEPARATOR, + T_STRING, + T_NS_SEPARATOR, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_NS_SEPARATOR, + T_STRING, + ], + ], + ]; + + // The constant name, as the last token in the sequence, is always T_STRING. + foreach ($data as $key => $value) { + $data[$key]['sequence'][] = T_STRING; + } + + return $data; + + }//end dataIntersectionTypedConstant() + + +}//end class