diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index e8ad0db793..61614ae59f 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; @@ -648,6 +650,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; @@ -681,6 +707,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()`. */ @@ -1851,6 +1897,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. @@ -2218,7 +2278,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 @@ -2990,6 +3073,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 @@ -3031,6 +3120,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 aedc49cc67..bd91af3680 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 9116782479..3ebc3daf65 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