From 5ce28e96f4fa1aa2f752e65bf3cf3a9311d85af3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 18 Apr 2024 17:19:50 +0200 Subject: [PATCH] PHP 8.2 | Tokenizer/PHP: add support for DNF types This commit adds tokenizer support for DNF types as per the proposal outlined in 387. This means that: * Two new tokens are introduced `T_TYPE_OPEN_PARENTHESIS` and `T_TYPE_CLOSE_PARENTHESIS` for the parentheses used in DNF types. This allows for sniffs to specifically target those tokens and prevents sniffs which are looking for the "normal" open/close parenthesis tokens from acting on DNF parentheses. * These new tokens, like other parentheses, will get the `parenthesis_opener` and `parenthesis_closer` token array indexes and the tokens between them will have the `nested_parenthesis` index. Based on the currently added tests, the commit safeguards that: * The `|` in types is still tokenized as `T_TYPE_UNION`, even in DNF types. * The `&` in types is still tokenized as `T_TYPE_INTERSECTION`, even in DNF types. * The `static` keyword for properties is still tokenized as `T_STATIC`, even when right before a DNF type (which could be confused for a function call). * The arrow function retokenization to `T_FN` with a `T_FN_ARROW` scope opener is handled correctly, even when DNF types are involved and including when the arrow function is declared to return by reference. * The keyword tokens, like `self`, `parent`, `static`, `true` or `false`, when used in DNF types are still tokenized to their own token and not tokenized as `T_STRING`. * The `array` keyword when used in DNF types is still tokenized as `T_STRING` and not as `T_ARRAY`. * A `?` intended as an (illegal) nullability operator in combination with a DNF type is still tokenized as `T_NULLABLE` and not as `T_INLINE_THEN`. * A function declaration open parenthesis before a typed parameter isn't accidentally retokenized to `T_TYPE_OPEN_PARENTHESIS`. Includes ample unit tests. Even so, strenuous testing of this PR is recommended as there are so many moving parts involved, it is very easy for something to have been overlooked. Related to 105 Closes 387 Closes squizlabs/PHP_CodeSniffer 3731 --- src/Tokenizers/PHP.php | 159 ++++-- src/Util/Tokens.php | 2 + tests/Core/Tokenizer/ArrayKeywordTest.inc | 17 + tests/Core/Tokenizer/ArrayKeywordTest.php | 18 + tests/Core/Tokenizer/BackfillFnTokenTest.inc | 9 + tests/Core/Tokenizer/BackfillFnTokenTest.php | 48 ++ tests/Core/Tokenizer/BitwiseOrTest.inc | 13 + tests/Core/Tokenizer/BitwiseOrTest.php | 39 +- .../ContextSensitiveKeywordsTest.inc | 7 + .../ContextSensitiveKeywordsTest.php | 5 + tests/Core/Tokenizer/DNFTypesTest.inc | 185 +++++++ tests/Core/Tokenizer/DNFTypesTest.php | 484 ++++++++++++++++++ .../OtherContextSensitiveKeywordsTest.inc | 42 ++ .../OtherContextSensitiveKeywordsTest.php | 84 +++ tests/Core/Tokenizer/TypeIntersectionTest.inc | 14 + tests/Core/Tokenizer/TypeIntersectionTest.php | 43 +- tests/Core/Tokenizer/TypedConstantsTest.inc | 24 + tests/Core/Tokenizer/TypedConstantsTest.php | 152 ++++++ 18 files changed, 1282 insertions(+), 63 deletions(-) create mode 100644 tests/Core/Tokenizer/DNFTypesTest.inc create mode 100644 tests/Core/Tokenizer/DNFTypesTest.php diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index dded2a5c17..6d99bba85d 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -464,6 +464,8 @@ class PHP extends Tokenizer T_CLOSE_SHORT_ARRAY => 1, T_TYPE_UNION => 1, T_TYPE_INTERSECTION => 1, + T_TYPE_OPEN_PARENTHESIS => 1, + T_TYPE_CLOSE_PARENTHESIS => 1, ]; /** @@ -747,6 +749,9 @@ protected function tokenize($string) /* Special case for `static` used as a function name, i.e. `static()`. + + Note: this may incorrectly change the static keyword directly before a DNF property type. + If so, this will be caught and corrected for in the additional processing. */ if ($tokenIsArray === true @@ -2712,21 +2717,23 @@ protected function processAdditional() if (isset($this->tokens[$x]) === true && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) { $ignore = Tokens::$emptyTokens; $ignore += [ - T_ARRAY => T_ARRAY, - T_CALLABLE => T_CALLABLE, - T_COLON => T_COLON, - T_NAMESPACE => T_NAMESPACE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_NULL => T_NULL, - T_TRUE => T_TRUE, - T_FALSE => T_FALSE, - T_NULLABLE => T_NULLABLE, - T_PARENT => T_PARENT, - T_SELF => T_SELF, - T_STATIC => T_STATIC, - T_STRING => T_STRING, - T_TYPE_UNION => T_TYPE_UNION, - T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, + T_ARRAY => T_ARRAY, + T_CALLABLE => T_CALLABLE, + T_COLON => T_COLON, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, + T_NULL => T_NULL, + T_TRUE => T_TRUE, + T_FALSE => T_FALSE, + T_NULLABLE => T_NULLABLE, + T_PARENT => T_PARENT, + T_SELF => T_SELF, + T_STATIC => T_STATIC, + T_STRING => T_STRING, + T_TYPE_UNION => T_TYPE_UNION, + T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, + T_TYPE_OPEN_PARENTHESIS => T_TYPE_OPEN_PARENTHESIS, + T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS, ]; $closer = $this->tokens[$x]['parenthesis_closer']; @@ -3029,10 +3036,15 @@ protected function processAdditional() continue; } else if ($this->tokens[$i]['code'] === T_BITWISE_OR || $this->tokens[$i]['code'] === T_BITWISE_AND + || $this->tokens[$i]['code'] === T_OPEN_PARENTHESIS + || $this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS ) { /* Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR. Convert "&" to T_TYPE_INTERSECTION or leave as T_BITWISE_AND. + Convert "(" and ")" to T_TYPE_(OPEN|CLOSE)_PARENTHESIS or leave as T_(OPEN|CLOSE)_PARENTHESIS. + + All type related tokens will be converted in one go as soon as this section is hit. */ $allowed = [ @@ -3048,8 +3060,8 @@ protected function processAdditional() T_NS_SEPARATOR => T_NS_SEPARATOR, ]; - $suspectedType = null; - $typeTokenCount = 0; + $suspectedType = null; + $typeTokenCountAfter = 0; for ($x = ($i + 1); $x < $numTokens; $x++) { if (isset(Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) { @@ -3057,11 +3069,13 @@ protected function processAdditional() } if (isset($allowed[$this->tokens[$x]['code']]) === true) { - ++$typeTokenCount; + ++$typeTokenCountAfter; continue; } - if ($typeTokenCount > 0 + if (($typeTokenCountAfter > 0 + || ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS + && isset($this->tokens[$i]['parenthesis_owner']) === false)) && ($this->tokens[$x]['code'] === T_BITWISE_AND || $this->tokens[$x]['code'] === T_ELLIPSIS) ) { @@ -3092,6 +3106,7 @@ protected function processAdditional() && $this->tokens[$this->tokens[$x]['scope_condition']]['code'] === T_FUNCTION ) { $suspectedType = 'return'; + break; } if ($this->tokens[$x]['code'] === T_EQUAL) { @@ -3103,8 +3118,12 @@ protected function processAdditional() break; }//end for - if ($typeTokenCount === 0 || isset($suspectedType) === false) { - // Definitely not a union or intersection type, move on. + if (($typeTokenCountAfter === 0 + && ($this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS + || isset($this->tokens[$i]['parenthesis_owner']) === true)) + || isset($suspectedType) === false + ) { + // Definitely not a union, intersection or DNF type, move on. continue; } @@ -3112,26 +3131,82 @@ protected function processAdditional() unset($allowed[T_STATIC]); } - $typeTokenCount = 0; - $typeOperators = [$i]; - $confirmed = false; + $typeTokenCountBefore = 0; + $typeOperators = [$i]; + $confirmed = false; + $maybeNullable = null; for ($x = ($i - 1); $x >= 0; $x--) { if (isset(Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) { continue; } + if ($suspectedType === 'property or parameter' + && $this->tokens[$x]['code'] === T_STRING + && strtolower($this->tokens[$x]['content']) === 'static' + ) { + // Static keyword followed directly by an open parenthesis for a DNF type. + // This token should be T_STATIC and was incorrectly identified as a function call before. + $this->tokens[$x]['code'] = T_STATIC; + $this->tokens[$x]['type'] = 'T_STATIC'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $line = $this->tokens[$x]['line']; + echo "\t* token $x on line $line changed back from T_STRING to T_STATIC".PHP_EOL; + } + } + + if ($suspectedType === 'property or parameter' + && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS + ) { + // We need to prevent the open parenthesis for a function/fn declaration from being retokenized + // to T_TYPE_OPEN_PARENTHESIS if this is the first parameter in the declaration. + if (isset($this->tokens[$x]['parenthesis_owner']) === true + && $this->tokens[$this->tokens[$x]['parenthesis_owner']]['code'] === T_FUNCTION + ) { + $confirmed = true; + break; + } else { + // This may still be an arrow function which hasn't be handled yet. + for ($y = ($x - 1); $y > 0; $y--) { + if (isset(Tokens::$emptyTokens[$this->tokens[$y]['code']]) === false + && $this->tokens[$y]['code'] !== T_BITWISE_AND + ) { + // Non-whitespace content. + break; + } + } + + if ($this->tokens[$y]['code'] === T_FN) { + $confirmed = true; + break; + } + } + }//end if + if (isset($allowed[$this->tokens[$x]['code']]) === true) { - ++$typeTokenCount; + ++$typeTokenCountBefore; continue; } - // Union and intersection types can't use the nullable operator, but be tolerant to parse errors. - if ($typeTokenCount > 0 && $this->tokens[$x]['code'] === T_NULLABLE) { + // Union, intersection and DNF types can't use the nullable operator, but be tolerant to parse errors. + if (($typeTokenCountBefore > 0 + || ($this->tokens[$x]['code'] === T_OPEN_PARENTHESIS && isset($this->tokens[$x]['parenthesis_owner']) === false)) + && ($this->tokens[$x]['code'] === T_NULLABLE + || $this->tokens[$x]['code'] === T_INLINE_THEN) + ) { + if ($this->tokens[$x]['code'] === T_INLINE_THEN) { + $maybeNullable = $x; + } + continue; } - if ($this->tokens[$x]['code'] === T_BITWISE_OR || $this->tokens[$x]['code'] === T_BITWISE_AND) { + if ($this->tokens[$x]['code'] === T_BITWISE_OR + || $this->tokens[$x]['code'] === T_BITWISE_AND + || $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS + || $this->tokens[$x]['code'] === T_CLOSE_PARENTHESIS + ) { $typeOperators[] = $x; continue; } @@ -3217,7 +3292,7 @@ protected function processAdditional() $line = $this->tokens[$x]['line']; echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL; } - } else { + } else if ($this->tokens[$x]['code'] === T_BITWISE_AND) { $this->tokens[$x]['code'] = T_TYPE_INTERSECTION; $this->tokens[$x]['type'] = 'T_TYPE_INTERSECTION'; @@ -3225,6 +3300,32 @@ protected function processAdditional() $line = $this->tokens[$x]['line']; echo "\t* token $x on line $line changed from T_BITWISE_AND to T_TYPE_INTERSECTION".PHP_EOL; } + } else if ($this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) { + $this->tokens[$x]['code'] = T_TYPE_OPEN_PARENTHESIS; + $this->tokens[$x]['type'] = 'T_TYPE_OPEN_PARENTHESIS'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $line = $this->tokens[$x]['line']; + echo "\t* token $x on line $line changed from T_OPEN_PARENTHESIS to T_TYPE_OPEN_PARENTHESIS".PHP_EOL; + } + } else if ($this->tokens[$x]['code'] === T_CLOSE_PARENTHESIS) { + $this->tokens[$x]['code'] = T_TYPE_CLOSE_PARENTHESIS; + $this->tokens[$x]['type'] = 'T_TYPE_CLOSE_PARENTHESIS'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $line = $this->tokens[$x]['line']; + echo "\t* token $x on line $line changed from T_CLOSE_PARENTHESIS to T_TYPE_CLOSE_PARENTHESIS".PHP_EOL; + } + }//end if + }//end foreach + + if (isset($maybeNullable) === true) { + $this->tokens[$maybeNullable]['code'] = T_NULLABLE; + $this->tokens[$maybeNullable]['type'] = 'T_NULLABLE'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $line = $this->tokens[$maybeNullable]['line']; + echo "\t* token $maybeNullable on line $line changed from T_INLINE_THEN to T_NULLABLE".PHP_EOL; } } diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php index ab70e78322..5ec913df2e 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -82,6 +82,8 @@ define('T_ATTRIBUTE_END', 'PHPCS_T_ATTRIBUTE_END'); define('T_ENUM_CASE', 'PHPCS_T_ENUM_CASE'); define('T_TYPE_INTERSECTION', 'PHPCS_T_TYPE_INTERSECTION'); +define('T_TYPE_OPEN_PARENTHESIS', 'PHPCS_T_TYPE_OPEN_PARENTHESIS'); +define('T_TYPE_CLOSE_PARENTHESIS', 'PHPCS_T_TYPE_CLOSE_PARENTHESIS'); // Some PHP 5.5 tokens, replicated for lower versions. if (defined('T_FINALLY') === false) { diff --git a/tests/Core/Tokenizer/ArrayKeywordTest.inc b/tests/Core/Tokenizer/ArrayKeywordTest.inc index ce211bda2a..6d8adfcbae 100644 --- a/tests/Core/Tokenizer/ArrayKeywordTest.inc +++ b/tests/Core/Tokenizer/ArrayKeywordTest.inc @@ -39,3 +39,20 @@ class Bar { /* testOOPropertyType */ protected array $property; } + +class DNFTypes { + /* testOOConstDNFType */ + const (A&B)|array|(C&D) NAME = []; + + /* testOOPropertyDNFType */ + protected (A&B)|ARRAY|null $property; + + /* testFunctionDeclarationParamDNFType */ + public function name(null|array|(A&B) $param) { + /* testClosureDeclarationParamDNFType */ + $cl = function ( array|(A&B) $param) {}; + + /* testArrowDeclarationReturnDNFType */ + $arrow = fn($a): (A&B)|Array => new $a; + } +} diff --git a/tests/Core/Tokenizer/ArrayKeywordTest.php b/tests/Core/Tokenizer/ArrayKeywordTest.php index f81706c330..c6dac85978 100644 --- a/tests/Core/Tokenizer/ArrayKeywordTest.php +++ b/tests/Core/Tokenizer/ArrayKeywordTest.php @@ -131,6 +131,24 @@ public static function dataArrayType() 'OO property type' => [ 'testMarker' => '/* testOOPropertyType */', ], + + 'OO constant DNF type' => [ + 'testMarker' => '/* testOOConstDNFType */', + ], + 'OO property DNF type' => [ + 'testMarker' => '/* testOOPropertyDNFType */', + 'testContent' => 'ARRAY', + ], + 'function param DNF type' => [ + 'testMarker' => '/* testFunctionDeclarationParamDNFType */', + ], + 'closure param DNF type' => [ + 'testMarker' => '/* testClosureDeclarationParamDNFType */', + ], + 'arrow return DNF type' => [ + 'testMarker' => '/* testArrowDeclarationReturnDNFType */', + 'testContent' => 'Array', + ], ]; }//end dataArrayType() diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.inc b/tests/Core/Tokenizer/BackfillFnTokenTest.inc index 63f9326836..cbb7b63bfc 100644 --- a/tests/Core/Tokenizer/BackfillFnTokenTest.inc +++ b/tests/Core/Tokenizer/BackfillFnTokenTest.inc @@ -119,6 +119,15 @@ $arrowWithUnionParam = fn(Traversable&Countable $param) : int => (new SomeClass( /* testIntersectionReturnType */ $arrowWithUnionReturn = fn($param) : \MyFoo&SomeInterface => new SomeClass($param); +/* testDNFParamType */ +$arrowWithUnionParam = fn((Traversable&Countable)|null $param) : SomeClass => new SomeClass($param) ?? null; + +/* testDNFReturnType */ +$arrowWithUnionReturn = fn($param) : false|(\MyFoo&SomeInterface) => new \MyFoo($param) ?? false; + +/* testDNFParamTypeWithReturnByRef */ +$arrowWithParamReturnByRef = fn &((A&B)|null $param) => $param * 10; + /* testTernary */ $fn = fn($a) => $a ? /* testTernaryThen */ fn() : string => 'a' : /* testTernaryElse */ fn() : string => 'b'; diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.php b/tests/Core/Tokenizer/BackfillFnTokenTest.php index f394276a7c..194a85102d 100644 --- a/tests/Core/Tokenizer/BackfillFnTokenTest.php +++ b/tests/Core/Tokenizer/BackfillFnTokenTest.php @@ -547,6 +547,54 @@ public function testIntersectionReturnType() }//end testIntersectionReturnType() + /** + * Test arrow function with a DNF parameter type. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testDNFParamType() + { + $token = $this->getTargetToken('/* testDNFParamType */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 17, 29); + + }//end testDNFParamType() + + + /** + * Test arrow function with a DNF return type. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testDNFReturnType() + { + $token = $this->getTargetToken('/* testDNFReturnType */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 16, 29); + + }//end testDNFReturnType() + + + /** + * Test arrow function which returns by reference with a DNF parameter type. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testDNFParamTypeWithReturnByRef() + { + $token = $this->getTargetToken('/* testDNFParamTypeWithReturnByRef */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 15, 22); + + }//end testDNFParamTypeWithReturnByRef() + + /** * Test arrow functions used in ternary operators. * diff --git a/tests/Core/Tokenizer/BitwiseOrTest.inc b/tests/Core/Tokenizer/BitwiseOrTest.inc index 5afc1e5bdf..54ff50822c 100644 --- a/tests/Core/Tokenizer/BitwiseOrTest.inc +++ b/tests/Core/Tokenizer/BitwiseOrTest.inc @@ -121,6 +121,19 @@ function globalFunctionWithSpreadAndReference( string|int ...$paramB ) {} +$dnfTypes = new class { + /* testTypeUnionConstantTypeUnionBeforeDNF */ + const Foo|(A&B) UNION_BEFORE = /* testBitwiseOrOOConstDefaultValueDNF */ Foo|(A&B); + + /* testTypeUnionPropertyTypeUnionAfterDNF */ + protected (\FQN&namespace\Relative)|Partially\Qualified $union_after = /* testBitwiseOrPropertyDefaultValueDNF */ (A&B)|Foo; + + public function unionBeforeAndAfter( + /* testTypeUnionParamUnionBeforeAndAfterDNF */ + string|(Stringable&\Countable)|int $param = /* testBitwiseOrParamDefaultValueDNF */ ( CONST_A & CONST_B) | CONST_C + ): /* testTypeUnionReturnTypeUnionAfterDNF */ (A&B)|null {} +}; + /* testTypeUnionClosureParamIllegalNullable */ $closureWithParamType = function (?string|null $string) {}; diff --git a/tests/Core/Tokenizer/BitwiseOrTest.php b/tests/Core/Tokenizer/BitwiseOrTest.php index 8e3e264f5b..ade73e1af4 100644 --- a/tests/Core/Tokenizer/BitwiseOrTest.php +++ b/tests/Core/Tokenizer/BitwiseOrTest.php @@ -45,22 +45,25 @@ public function testBitwiseOr($testMarker) 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 */'], - 'in closure parameter default value' => ['/* testBitwiseOrClosureParamDefault */'], - 'in arrow function parameter default value' => ['/* testBitwiseOrArrowParamDefault */'], - 'in arrow function return expression' => ['/* testBitwiseOrArrowExpression */'], - 'in long array key' => ['/* testBitwiseOrInArrayKey */'], - 'in long array value' => ['/* testBitwiseOrInArrayValue */'], - 'in short array key' => ['/* testBitwiseOrInShortArrayKey */'], - 'in short array value' => ['/* testBitwiseOrInShortArrayValue */'], - 'in catch condition' => ['/* testBitwiseOrTryCatch */'], - 'in parameter in function call' => ['/* testBitwiseOrNonArrowFnFunctionCall */'], - 'live coding / undetermined' => ['/* testLiveCoding */'], + '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 */'], + 'in closure parameter default value' => ['/* testBitwiseOrClosureParamDefault */'], + 'in OO constant default value DNF-like' => ['/* testBitwiseOrOOConstDefaultValueDNF */'], + 'in property default value DNF-like' => ['/* testBitwiseOrPropertyDefaultValueDNF */'], + 'in method parameter default value DNF-like' => ['/* testBitwiseOrParamDefaultValueDNF */'], + 'in arrow function parameter default value' => ['/* testBitwiseOrArrowParamDefault */'], + 'in arrow function return expression' => ['/* testBitwiseOrArrowExpression */'], + 'in long array key' => ['/* testBitwiseOrInArrayKey */'], + 'in long array value' => ['/* testBitwiseOrInArrayValue */'], + 'in short array key' => ['/* testBitwiseOrInShortArrayKey */'], + 'in short array value' => ['/* testBitwiseOrInShortArrayValue */'], + 'in catch condition' => ['/* testBitwiseOrTryCatch */'], + 'in parameter in function call' => ['/* testBitwiseOrNonArrowFnFunctionCall */'], + 'live coding / undetermined' => ['/* testLiveCoding */'], ]; }//end dataBitwiseOr() @@ -135,6 +138,10 @@ public static function dataTypeUnion() 'return type for method with fully qualified names' => ['/* testTypeUnionReturnFullyQualified */'], 'type for function parameter with reference' => ['/* testTypeUnionWithReference */'], 'type for function parameter with spread operator' => ['/* testTypeUnionWithSpreadOperator */'], + 'DNF type for OO constant, union before DNF' => ['/* testTypeUnionConstantTypeUnionBeforeDNF */'], + 'DNF type for property, union after DNF' => ['/* testTypeUnionPropertyTypeUnionAfterDNF */'], + 'DNF type for function param, union before and after DNF' => ['/* testTypeUnionParamUnionBeforeAndAfterDNF */'], + 'DNF type for function return, union after DNF with null' => ['/* testTypeUnionReturnTypeUnionAfterDNF */'], 'type for closure parameter with illegal nullable' => ['/* testTypeUnionClosureParamIllegalNullable */'], 'return type for closure' => ['/* testTypeUnionClosureReturn */'], 'type for arrow function parameter' => ['/* testTypeUnionArrowParam */'], diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc index 2d471285c2..2825f26e52 100644 --- a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc @@ -235,3 +235,10 @@ $obj-> /* testKeywordAsMethodCallNameShouldBeStringStatic */ static(); $function = /* testStaticIsKeywordBeforeClosure */ static function(/* testStaticIsKeywordWhenParamType */ static $param) {}; $arrow = /* testStaticIsKeywordBeforeArrow */ static fn(): /* testStaticIsKeywordWhenReturnType */ static => 10; + +/* testKeywordAsFunctionCallNameShouldBeStringStaticDNFLookaLike */ +$obj->static((CONST_A&CONST_B)|CONST_C | $var); + +class DNF { + public /* testStaticIsKeywordPropertyModifierBeforeDNF */ static (DN&F)|null $dnfProp; +} diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php index 51c5453873..f70b9caa89 100644 --- a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php @@ -135,6 +135,7 @@ public static function dataStrings() 'function call: static' => ['/* testKeywordAsFunctionCallNameShouldBeStringStatic */'], 'method call: static' => ['/* testKeywordAsMethodCallNameShouldBeStringStatic */'], + 'method call: static with dnf look a like param' => ['/* testKeywordAsFunctionCallNameShouldBeStringStaticDNFLookaLike */'], ]; }//end dataStrings() @@ -534,6 +535,10 @@ public static function dataKeywords() 'testMarker' => '/* testStaticIsKeywordWhenReturnType */', 'expectedTokenType' => 'T_STATIC', ], + 'static: property modifier before DNF' => [ + 'testMarker' => '/* testStaticIsKeywordPropertyModifierBeforeDNF */', + 'expectedTokenType' => 'T_STATIC', + ], ]; }//end dataKeywords() diff --git a/tests/Core/Tokenizer/DNFTypesTest.inc b/tests/Core/Tokenizer/DNFTypesTest.inc new file mode 100644 index 0000000000..6c671ebfdf --- /dev/null +++ b/tests/Core/Tokenizer/DNFTypesTest.inc @@ -0,0 +1,185 @@ + 10 ) {} + +/* testParensOwnerFor */ +for ($i =0; $i < /* testParensNoOwnerInForCondition */ ( CONST_A & CONST_B ); $i++ ); + +/* testParensOwnerMatch */ +$match = match(CONST_A & CONST_B) { + default => $a, +}; + +/* testParensOwnerArray */ +$array = array ( + 'text', + \CONST_A & \Fully\Qualified\CONST_B, + /* testParensNoOwnerFunctionCallWithAmpersandInCallable */ + do_something($a, /* testParensOwnerArrowFn */ fn($b) => $a & $b, $c), +); + +/* testParensOwnerListWithRefVars */ +list(&$a, &$b) = $array; + +/* testParensNoOwnerFunctionCallwithDNFLookALikeParam */ +$obj->static((CONST_A&CONST_B)|CONST_C | $var); + + +/* + * DNF parentheses. + */ + +class DNFTypes { + /* testDNFTypeOOConstUnqualifiedClasses */ + public const (A&B)|D UNQUALIFIED = new Foo; + + /* testDNFTypeOOConstReverseModifierOrder */ + protected final const int|(Foo&Bar)|float MODIFIERS_REVERSED /* testParensNoOwnerOOConstDefaultValue */ = (E_WARNING & E_NOTICE) | E_DEPRECATED; + + const + /* testDNFTypeOOConstMulti1 */ + (A&B) | + /* testDNFTypeOOConstMulti2 */ + (C&D) | // phpcs:ignore Stnd.Cat.Sniff + /* testDNFTypeOOConstMulti3 */ + (Y&D) + | null MULTI_DNF = false; + + /* testDNFTypeOOConstNamespaceRelative */ + final protected const (namespace\Sub\NameA&namespace\Sub\NameB)|namespace\Sub\NameC NAMESPACE_RELATIVE = new namespace\Sub\NameB; + + /* testDNFTypeOOConstPartiallyQualified */ + const Partially\Qualified\NameC|(Partially\Qualified\NameA&Partially\Qualified\NameB) PARTIALLY_QUALIFIED = new Partially\Qualified\NameA; + + /* testDNFTypeOOConstFullyQualified */ + const (\Fully\Qualified\NameA&\Fully\Qualified\NameB)|\Fully\Qualified\NameC FULLY_QUALIFIED = new \Fully\Qualified\NameB(); + + /* testDNFTypePropertyUnqualifiedClasses */ + public static (Foo&Bar)|object $obj; + + /* testDNFTypePropertyReverseModifierOrder */ + static protected string|(A&B)|bool $dnf /* testParensNoOwnerPropertyDefaultValue1 */ = ( E_WARNING & E_NOTICE ) | /* testParensNoOwnerPropertyDefaultValue2 */ (E_ALL & E_DEPRECATED); + + private + /* testDNFTypePropertyMultiNamespaceRelative */ + (namespace\Sub\NameA&namespace\Sub\NameB) | + /* testDNFTypePropertyMultiPartiallyQualified */ + (Partially\Qualified\NameA&Partially\Qualified\NameB) | // phpcs:ignore Stnd.Cat.Sniff + false + /* testDNFTypePropertyMultiFullyQualified */ + | (\Fully\Qualified\NameA&\Fully\Qualified\NameB) $multiDnf; + + /* testDNFTypePropertyWithReadOnlyKeyword1 */ + protected readonly (A&B) | /* testDNFTypePropertyWithReadOnlyKeyword2 */ (C&D) $readonly; + + /* testDNFTypePropertyWithStaticAndReadOnlyKeywords */ + static readonly (A&B&C)|array $staticReadonly; + + /* testDNFTypePropertyWithOnlyStaticKeyword */ + static (A&B&C)|true $obj; + + public function paramTypes( + /* testDNFTypeParam1WithAttribute */ + #[MyAttribute] + (\Foo&Bar)|int|float $paramA /* testParensNoOwnerParamDefaultValue */ = SOMETHING | (CONSTANT_A & CONSTANT_B), + + /* testDNFTypeParam2 */ + (Foo&\Bar) /* testDNFTypeParam3 */ |(Baz&Fop) &...$paramB = null, + ) { + /* testParensNoOwnerInReturnValue1 */ + return ( + /* testParensNoOwnerInReturnValue2 */ + ($a1 & $b1) | + /* testParensNoOwnerInReturnValue3 */ + ($a2 & $b2) + ) + $c; + } + + public function identifierNames( + /* testDNFTypeParamNamespaceRelative */ + (namespace\Sub\NameA&namespace\Sub\NameB)|false $paramA, + /* testDNFTypeParamPartiallyQualified */ + Partially\Qualified\NameC|(Partially\Qualified\NameA&Partially\Qualified\NameB) $paramB, + /* testDNFTypeParamFullyQualified */ + name|(\Fully\Qualified\NameA&\Fully\Qualified\NameB) $paramC, + ) {} + + public function __construct( + /* testDNFTypeConstructorPropertyPromotion1 */ + public (A&B)| /* testDNFTypeConstructorPropertyPromotion2 */ (A&D) $property + ) {} + + public function returnType()/* testDNFTypeReturnType1 */ : A|(B&D)|/* testDNFTypeReturnType2 */(B&W)|null {} + + abstract public function abstractMethod(): /* testDNFTypeAbstractMethodReturnType1 */ (X&Y) /* testDNFTypeAbstractMethodReturnType2 */ |(W&Z); + + public function identifierNamesReturnRelative( + ) : /* testDNFTypeReturnTypeNamespaceRelative */ (namespace\Sub\NameA&namespace\Sub\NameB)|namespace\Sub\NameC {} + + public function identifierNamesReturnPQ( + ) : /* testDNFTypeReturnPartiallyQualified */Partially\Qualified\NameA|(Partially\Qualified\NameB&Partially\Qualified\NameC) {} + + // Illegal type: segments which are strict subsets of others are disallowed, but that's not the concern of the tokenizer. + public function identifierNamesReturnFQ( + ) /* testDNFTypeReturnFullyQualified */ : (\Fully\Qualified\NameA&\Fully\Qualified\NameB)|\Fully\Qualified\NameB {} +} + +function globalFunctionWithSpreadAndReference( + /* testDNFTypeWithReference */ + float|(B&A) &$paramA, + /* testDNFTypeWithSpreadOperator */ + string|(B|D) ...$paramB +) {} + + +$closureWithParamType = function ( /* testDNFTypeClosureParamIllegalNullable */ ?(A&B)|bool $string) {}; + +/* testParensOwnerClosureAmpersandInDefaultValue */ +$closureWithReturnType = function ($string = NONSENSE & FAKE) /* testDNFTypeClosureReturn */ : (\Package\MyA&PackageB)|null {}; + +/* testParensOwnerArrowDNFUsedWithin */ +$arrowWithParamType = fn ( + /* testDNFTypeArrowParam */ + object|(A&B&C)|array $param, + /* testParensNoOwnerAmpersandInDefaultValue */ ?int $int = (CONSTA & CONSTB )| CONST_C +) + /* testParensNoOwnerInArrowReturnExpression */ + => ($param & $foo ) | $int; + +$arrowWithReturnType = fn ($param) : /* testDNFTypeArrowReturnType */ int|(A&B) => $param * 10; + +$arrowWithParamReturnByRef = fn &( + /* testDNFTypeArrowParamWithReturnByRef */ + (A&B)|null $param +) => $param * 10; + +function InvalidSyntaxes( + /* testDNFTypeParamIllegalUnnecessaryParens */ + (A&B) $parensNotNeeded + + /* testDNFTypeParamIllegalIntersectUnionReversed */ + A&(B|D) $onlyIntersectAllowedWithinParensAndUnionOutside + + /* testDNFTypeParamIllegalNestedParens */ + A|(B&(D|W)|null) $nestedParensNotAllowed +) {} diff --git a/tests/Core/Tokenizer/DNFTypesTest.php b/tests/Core/Tokenizer/DNFTypesTest.php new file mode 100644 index 0000000000..a0474e2fd0 --- /dev/null +++ b/tests/Core/Tokenizer/DNFTypesTest.php @@ -0,0 +1,484 @@ + + * @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 DNFTypesTest extends AbstractTokenizerTestCase +{ + + + /** + * Test that parentheses when **not** used in a type declaration are correctly tokenized. + * + * @param string $testMarker The comment prefacing the target token. + * @param int|false $owner Optional. The parentheses owner or false when no parentheses owner is expected. + * @param bool $skipCheckInside Optional. Skip checking correct token type inside the parentheses. + * Use judiciously for combined normal + DNF tests only. + * + * @dataProvider dataNormalParentheses + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createParenthesisNestingMap + * + * @return void + */ + public function testNormalParentheses($testMarker, $owner=false, $skipCheckInside=false) + { + $tokens = $this->phpcsFile->getTokens(); + + $openPtr = $this->getTargetToken($testMarker, [T_OPEN_PARENTHESIS, T_TYPE_OPEN_PARENTHESIS]); + $opener = $tokens[$openPtr]; + + $this->assertSame('(', $opener['content'], 'Content of type open parenthesis is not "("'); + $this->assertSame(T_OPEN_PARENTHESIS, $opener['code'], 'Token tokenized as '.$opener['type'].', not T_OPEN_PARENTHESIS (code)'); + $this->assertSame('T_OPEN_PARENTHESIS', $opener['type'], 'Token tokenized as '.$opener['type'].', not T_OPEN_PARENTHESIS (type)'); + + if ($owner !== false) { + $this->assertArrayHasKey('parenthesis_owner', $opener, 'Parenthesis owner is not set'); + $this->assertSame(($openPtr + $owner), $opener['parenthesis_owner'], 'Opener parenthesis owner is not the expected token'); + } else { + $this->assertArrayNotHasKey('parenthesis_owner', $opener, 'Parenthesis owner is set'); + } + + $this->assertArrayHasKey('parenthesis_opener', $opener, 'Parenthesis opener is not set'); + $this->assertArrayHasKey('parenthesis_closer', $opener, 'Parenthesis closer is not set'); + $this->assertSame($openPtr, $opener['parenthesis_opener'], 'Parenthesis opener is not the expected token'); + + $closePtr = $opener['parenthesis_closer']; + $closer = $tokens[$closePtr]; + + $this->assertSame(')', $closer['content'], 'Content of type close parenthesis is not ")"'); + $this->assertSame(T_CLOSE_PARENTHESIS, $closer['code'], 'Token tokenized as '.$closer['type'].', not T_CLOSE_PARENTHESIS (code)'); + $this->assertSame('T_CLOSE_PARENTHESIS', $closer['type'], 'Token tokenized as '.$closer['type'].', not T_CLOSE_PARENTHESIS (type)'); + + if ($owner !== false) { + $this->assertArrayHasKey('parenthesis_owner', $closer, 'Parenthesis owner is not set'); + $this->assertSame(($openPtr + $owner), $closer['parenthesis_owner'], 'Closer parenthesis owner is not the expected token'); + } else { + $this->assertArrayNotHasKey('parenthesis_owner', $closer, 'Parenthesis owner is set'); + } + + $this->assertArrayHasKey('parenthesis_opener', $closer, 'Parenthesis opener is not set'); + $this->assertArrayHasKey('parenthesis_closer', $closer, 'Parenthesis closer is not set'); + $this->assertSame($closePtr, $closer['parenthesis_closer'], 'Parenthesis closer is not the expected token'); + + for ($i = ($openPtr + 1); $i < $closePtr; $i++) { + $this->assertArrayHasKey('nested_parenthesis', $tokens[$i], "Nested parenthesis key not set on token $i ({$tokens[$i]['type']})"); + $this->assertArrayHasKey($openPtr, $tokens[$i]['nested_parenthesis'], 'Nested parenthesis is missing target parentheses set'); + $this->assertSame($closePtr, $tokens[$i]['nested_parenthesis'][$openPtr], 'Nested parenthesis closer not set correctly'); + + // If there are ampersands, make sure these are tokenized as bitwise and. + if ($skipCheckInside === false && $tokens[$i]['content'] === '&') { + $this->assertSame(T_BITWISE_AND, $tokens[$i]['code'], 'Token tokenized as '.$tokens[$i]['type'].', not T_BITWISE_AND (code)'); + $this->assertSame('T_BITWISE_AND', $tokens[$i]['type'], 'Token tokenized as '.$tokens[$i]['type'].', not T_BITWISE_AND (type)'); + } + } + + $before = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($openPtr - 1), null, true); + if ($before !== false && $tokens[$before]['content'] === '|') { + $this->assertSame( + T_BITWISE_OR, + $tokens[$before]['code'], + 'Token before tokenized as '.$tokens[$before]['type'].', not T_BITWISE_OR (code)' + ); + $this->assertSame( + 'T_BITWISE_OR', + $tokens[$before]['type'], + 'Token before tokenized as '.$tokens[$before]['type'].', not T_BITWISE_OR (type)' + ); + } + + $after = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($closePtr + 1), null, true); + if ($after !== false && $tokens[$after]['content'] === '|') { + $this->assertSame( + T_BITWISE_OR, + $tokens[$after]['code'], + 'Token after tokenized as '.$tokens[$after]['type'].', not T_BITWISE_OR (code)' + ); + $this->assertSame( + 'T_BITWISE_OR', + $tokens[$after]['type'], + 'Token after tokenized as '.$tokens[$after]['type'].', not T_BITWISE_OR (type)' + ); + } + + }//end testNormalParentheses() + + + /** + * Data provider. + * + * @see testNormalParentheses() + * + * @return array> + */ + public static function dataNormalParentheses() + { + // "Owner" offsets are relative to the open parenthesis. + return [ + 'parens without owner' => [ + 'testMarker' => '/* testParensNoOwner */', + ], + 'parens without owner in ternary then' => [ + 'testMarker' => '/* testParensNoOwnerInTernary */', + ], + 'parens without owner in short ternary' => [ + 'testMarker' => '/* testParensNoOwnerInShortTernary */', + ], + 'parens with owner: function; & in default value' => [ + 'testMarker' => '/* testParensOwnerFunctionAmpersandInDefaultValue */', + 'owner' => -3, + ], + 'parens with owner: closure; param declared by & ref' => [ + 'testMarker' => '/* testParensOwnerClosureAmpersandParamRef */', + 'owner' => -1, + ], + 'parens with owner: if' => [ + 'testMarker' => '/* testParensOwnerIf */', + 'owner' => -2, + ], + 'parens without owner in if condition' => [ + 'testMarker' => '/* testParensNoOwnerInIfCondition */', + ], + 'parens with owner: for' => [ + 'testMarker' => '/* testParensOwnerFor */', + 'owner' => -2, + ], + 'parens without owner in for condition' => [ + 'testMarker' => '/* testParensNoOwnerInForCondition */', + ], + 'parens with owner: match' => [ + 'testMarker' => '/* testParensOwnerMatch */', + 'owner' => -1, + ], + 'parens with owner: array' => [ + 'testMarker' => '/* testParensOwnerArray */', + 'owner' => -2, + ], + 'parens without owner in array; function call with & in callable' => [ + 'testMarker' => '/* testParensNoOwnerFunctionCallWithAmpersandInCallable */', + ], + 'parens with owner: fn; & in return value' => [ + 'testMarker' => '/* testParensOwnerArrowFn */', + 'owner' => -1, + ], + 'parens with owner: list with reference vars' => [ + 'testMarker' => '/* testParensOwnerListWithRefVars */', + 'owner' => -1, + ], + 'parens without owner, function call with DNF look-a-like param' => [ + 'testMarker' => '/* testParensNoOwnerFunctionCallwithDNFLookALikeParam */', + ], + + 'parens without owner in OO const default value' => [ + 'testMarker' => '/* testParensNoOwnerOOConstDefaultValue */', + ], + 'parens without owner in property default 1' => [ + 'testMarker' => '/* testParensNoOwnerPropertyDefaultValue1 */', + ], + 'parens without owner in property default 2' => [ + 'testMarker' => '/* testParensNoOwnerPropertyDefaultValue2 */', + ], + 'parens without owner in param default value' => [ + 'testMarker' => '/* testParensNoOwnerParamDefaultValue */', + ], + 'parens without owner in return statement 1' => [ + 'testMarker' => '/* testParensNoOwnerInReturnValue1 */', + ], + 'parens without owner in return statement 2' => [ + 'testMarker' => '/* testParensNoOwnerInReturnValue2 */', + ], + 'parens without owner in return statement 3' => [ + 'testMarker' => '/* testParensNoOwnerInReturnValue3 */', + ], + 'parens with owner: closure; & in default value' => [ + 'testMarker' => '/* testParensOwnerClosureAmpersandInDefaultValue */', + 'owner' => -2, + ], + 'parens with owner: fn; dnf used within' => [ + 'testMarker' => '/* testParensOwnerArrowDNFUsedWithin */', + 'owner' => -2, + 'skipCheckInside' => true, + ], + 'parens without owner: default value for param in arrow function' => [ + 'testMarker' => '/* testParensNoOwnerAmpersandInDefaultValue */', + ], + 'parens without owner in arrow function return expression' => [ + 'testMarker' => '/* testParensNoOwnerInArrowReturnExpression */', + ], + ]; + + }//end dataNormalParentheses() + + + /** + * Test that parentheses when used in a DNF type declaration are correctly tokenized. + * + * Includes verifying that: + * - the tokens between the parentheses all have a "nested_parenthesis" key. + * - all ampersands between the parentheses are tokenized as T_TYPE_INTERSECTION. + * + * @param string $testMarker The comment prefacing the target token. + * + * @dataProvider dataDNFTypeParentheses + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createParenthesisNestingMap + * + * @return void + */ + public function testDNFTypeParentheses($testMarker) + { + $tokens = $this->phpcsFile->getTokens(); + + $openPtr = $this->getTargetToken($testMarker, [T_OPEN_PARENTHESIS, T_TYPE_OPEN_PARENTHESIS]); + $opener = $tokens[$openPtr]; + + $this->assertSame('(', $opener['content'], 'Content of type open parenthesis is not "("'); + $this->assertSame(T_TYPE_OPEN_PARENTHESIS, $opener['code'], 'Token tokenized as '.$opener['type'].', not T_TYPE_OPEN_PARENTHESIS (code)'); + $this->assertSame('T_TYPE_OPEN_PARENTHESIS', $opener['type'], 'Token tokenized as '.$opener['type'].', not T_TYPE_OPEN_PARENTHESIS (type)'); + + $this->assertArrayNotHasKey('parenthesis_owner', $opener, 'Parenthesis owner is set'); + $this->assertArrayHasKey('parenthesis_opener', $opener, 'Parenthesis opener is not set'); + $this->assertArrayHasKey('parenthesis_closer', $opener, 'Parenthesis closer is not set'); + $this->assertSame($openPtr, $opener['parenthesis_opener'], 'Parenthesis opener is not the expected token'); + + $closePtr = $opener['parenthesis_closer']; + $closer = $tokens[$closePtr]; + + $this->assertSame(')', $closer['content'], 'Content of type close parenthesis is not ")"'); + $this->assertSame(T_TYPE_CLOSE_PARENTHESIS, $closer['code'], 'Token tokenized as '.$closer['type'].', not T_TYPE_CLOSE_PARENTHESIS (code)'); + $this->assertSame('T_TYPE_CLOSE_PARENTHESIS', $closer['type'], 'Token tokenized as '.$closer['type'].', not T_TYPE_CLOSE_PARENTHESIS (type)'); + + $this->assertArrayNotHasKey('parenthesis_owner', $closer, 'Parenthesis owner is set'); + $this->assertArrayHasKey('parenthesis_opener', $closer, 'Parenthesis opener is not set'); + $this->assertArrayHasKey('parenthesis_closer', $closer, 'Parenthesis closer is not set'); + $this->assertSame($closePtr, $closer['parenthesis_closer'], 'Parenthesis closer is not the expected token'); + + $intersectionCount = 0; + for ($i = ($openPtr + 1); $i < $closePtr; $i++) { + $this->assertArrayHasKey('nested_parenthesis', $tokens[$i], "Nested parenthesis key not set on token $i ({$tokens[$i]['type']})"); + $this->assertArrayHasKey($openPtr, $tokens[$i]['nested_parenthesis'], 'Nested parenthesis is missing target parentheses set'); + $this->assertSame($closePtr, $tokens[$i]['nested_parenthesis'][$openPtr], 'Nested parenthesis closer not set correctly'); + + if ($tokens[$i]['content'] === '&') { + $this->assertSame( + T_TYPE_INTERSECTION, + $tokens[$i]['code'], + 'Token tokenized as '.$tokens[$i]['type'].', not T_TYPE_INTERSECTION (code)' + ); + $this->assertSame( + 'T_TYPE_INTERSECTION', + $tokens[$i]['type'], + 'Token tokenized as '.$tokens[$i]['type'].', not T_TYPE_INTERSECTION (type)' + ); + ++$intersectionCount; + } + + // Not valid, but that's irrelevant for the tokenization. + if ($tokens[$i]['content'] === '|') { + $this->assertSame(T_TYPE_UNION, $tokens[$i]['code'], 'Token tokenized as '.$tokens[$i]['type'].', not T_TYPE_UNION (code)'); + $this->assertSame('T_TYPE_UNION', $tokens[$i]['type'], 'Token tokenized as '.$tokens[$i]['type'].', not T_TYPE_UNION (type)'); + + // For the purposes of this test, presume it was intended as an intersection. + ++$intersectionCount; + } + }//end for + + $this->assertGreaterThanOrEqual(1, $intersectionCount, 'Did not find an intersection "&" between the DNF type parentheses'); + + $before = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($openPtr - 1), null, true); + if ($before !== false && $tokens[$before]['content'] === '|') { + $this->assertSame( + T_TYPE_UNION, + $tokens[$before]['code'], + 'Token before tokenized as '.$tokens[$before]['type'].', not T_TYPE_UNION (code)' + ); + $this->assertSame( + 'T_TYPE_UNION', + $tokens[$before]['type'], + 'Token before tokenized as '.$tokens[$before]['type'].', not T_TYPE_UNION (type)' + ); + } + + // Invalid, but that's not relevant for the tokenization. + if ($before !== false && $tokens[$before]['content'] === '?') { + $this->assertSame( + T_NULLABLE, + $tokens[$before]['code'], + 'Token before tokenized as '.$tokens[$before]['type'].', not T_NULLABLE (code)' + ); + $this->assertSame( + 'T_NULLABLE', + $tokens[$before]['type'], + 'Token before tokenized as '.$tokens[$before]['type'].', not T_NULLABLE (type)' + ); + } + + $after = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($closePtr + 1), null, true); + if ($after !== false && $tokens[$after]['content'] === '|') { + $this->assertSame( + T_TYPE_UNION, + $tokens[$after]['code'], + 'Token after tokenized as '.$tokens[$after]['type'].', not T_TYPE_UNION (code)' + ); + $this->assertSame( + 'T_TYPE_UNION', + $tokens[$after]['type'], + 'Token after tokenized as '.$tokens[$after]['type'].', not T_TYPE_UNION (type)' + ); + } + + }//end testDNFTypeParentheses() + + + /** + * Data provider. + * + * @see testDNFTypeParentheses() + * + * @return array> + */ + public static function dataDNFTypeParentheses() + { + return [ + 'OO const type: unqualified classes' => [ + 'testMarker' => '/* testDNFTypeOOConstUnqualifiedClasses */', + ], + 'OO const type: modifiers in reverse order' => [ + 'testMarker' => '/* testDNFTypeOOConstReverseModifierOrder */', + ], + 'OO const type: multi-dnf part 1' => [ + 'testMarker' => '/* testDNFTypeOOConstMulti1 */', + ], + 'OO const type: multi-dnf part 2' => [ + 'testMarker' => '/* testDNFTypeOOConstMulti2 */', + ], + 'OO const type: multi-dnf part 3' => [ + 'testMarker' => '/* testDNFTypeOOConstMulti3 */', + ], + 'OO const type: namespace relative classes' => [ + 'testMarker' => '/* testDNFTypeOOConstNamespaceRelative */', + ], + 'OO const type: partially qualified classes' => [ + 'testMarker' => '/* testDNFTypeOOConstPartiallyQualified */', + ], + 'OO const type: fully qualified classes' => [ + 'testMarker' => '/* testDNFTypeOOConstFullyQualified */', + ], + + 'OO property type: unqualified classes' => [ + 'testMarker' => '/* testDNFTypePropertyUnqualifiedClasses */', + ], + 'OO property type: modifiers in reverse order' => [ + 'testMarker' => '/* testDNFTypePropertyReverseModifierOrder */', + ], + 'OO property type: multi-dnf namespace relative classes' => [ + 'testMarker' => '/* testDNFTypePropertyMultiNamespaceRelative */', + ], + 'OO property type: multi-dnf partially qualified classes' => [ + 'testMarker' => '/* testDNFTypePropertyMultiPartiallyQualified */', + ], + 'OO property type: multi-dnf fully qualified classes' => [ + 'testMarker' => '/* testDNFTypePropertyMultiFullyQualified */', + ], + + 'OO property type: multi-dnf with readonly keyword 1' => [ + 'testMarker' => '/* testDNFTypePropertyWithReadOnlyKeyword1 */', + ], + 'OO property type: multi-dnf with readonly keyword 2' => [ + 'testMarker' => '/* testDNFTypePropertyWithReadOnlyKeyword2 */', + ], + 'OO property type: with static and readonly keywords' => [ + 'testMarker' => '/* testDNFTypePropertyWithStaticAndReadOnlyKeywords */', + ], + 'OO property type: with only static keyword' => [ + 'testMarker' => '/* testDNFTypePropertyWithOnlyStaticKeyword */', + ], + 'OO method param type: first param' => [ + 'testMarker' => '/* testDNFTypeParam1WithAttribute */', + ], + 'OO method param type: second param, first DNF' => [ + 'testMarker' => '/* testDNFTypeParam2 */', + ], + 'OO method param type: second param, second DNF' => [ + 'testMarker' => '/* testDNFTypeParam3 */', + ], + 'OO method param type: namespace relative classes' => [ + 'testMarker' => '/* testDNFTypeParamNamespaceRelative */', + ], + 'OO method param type: partially qualified classes' => [ + 'testMarker' => '/* testDNFTypeParamPartiallyQualified */', + ], + 'OO method param type: fully qualified classes' => [ + 'testMarker' => '/* testDNFTypeParamFullyQualified */', + ], + 'Constructor property promotion with multi DNF 1' => [ + 'testMarker' => '/* testDNFTypeConstructorPropertyPromotion1 */', + ], + 'Constructor property promotion with multi DNF 2' => [ + 'testMarker' => '/* testDNFTypeConstructorPropertyPromotion2 */', + ], + 'OO method return type: multi DNF 1' => [ + 'testMarker' => '/* testDNFTypeReturnType1 */', + ], + 'OO method return type: multi DNF 2' => [ + 'testMarker' => '/* testDNFTypeReturnType2 */', + ], + 'OO abstract method return type: multi DNF 1' => [ + 'testMarker' => '/* testDNFTypeAbstractMethodReturnType1 */', + ], + 'OO abstract method return type: multi DNF 2' => [ + 'testMarker' => '/* testDNFTypeAbstractMethodReturnType2 */', + ], + 'OO method return type: namespace relative classes' => [ + 'testMarker' => '/* testDNFTypeReturnTypeNamespaceRelative */', + ], + 'OO method return type: partially qualified classes' => [ + 'testMarker' => '/* testDNFTypeReturnPartiallyQualified */', + ], + 'OO method return type: fully qualified classes' => [ + 'testMarker' => '/* testDNFTypeReturnFullyQualified */', + ], + 'function param type: with reference' => [ + 'testMarker' => '/* testDNFTypeWithReference */', + ], + 'function param type: with spread' => [ + 'testMarker' => '/* testDNFTypeWithSpreadOperator */', + ], + 'closure param type: with illegal nullable' => [ + 'testMarker' => '/* testDNFTypeClosureParamIllegalNullable */', + ], + 'closure return type' => [ + 'testMarker' => '/* testDNFTypeClosureReturn */', + ], + 'arrow function param type' => [ + 'testMarker' => '/* testDNFTypeArrowParam */', + ], + 'arrow function return type' => [ + 'testMarker' => '/* testDNFTypeArrowReturnType */', + ], + 'arrow function param type with return by ref' => [ + 'testMarker' => '/* testDNFTypeArrowParamWithReturnByRef */', + ], + + 'illegal syntax: unnecessary parentheses (no union)' => [ + 'testMarker' => '/* testDNFTypeParamIllegalUnnecessaryParens */', + ], + 'illegal syntax: union within parentheses, intersect outside' => [ + 'testMarker' => '/* testDNFTypeParamIllegalIntersectUnionReversed */', + ], + 'illegal syntax: nested parentheses' => [ + 'testMarker' => '/* testDNFTypeParamIllegalNestedParens */', + ], + ]; + + }//end dataDNFTypeParentheses() + + +}//end class diff --git a/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc index 8fe91f1ca1..ef89caaeaa 100644 --- a/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc +++ b/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc @@ -203,3 +203,45 @@ class UseInIntersectionTypes { function SelfIsKeywordInReturnTypeLast(): Foo&/* testSelfIsKeywordAsReturnIntersectionTypeLast */self {} function ParentIsKeywordInReturnTypeLast(): bool&/* testParentIsKeywordAsReturnIntersectionTypeLast */parent {} } + +// Note: self/parent/static are rarely allowed in DNF types, but that is not the concern of the tokenizer. +class DNFTypes extends Something { + const (A&B)/* testFalseIsKeywordAsConstDNFType */|false NAME_FALSE = SOME_CONST; + const /* testTrueIsKeywordAsConstDNFType */ true|(A&B) NAME_TRUE = SOME_CONST; + const (A&B)|/* testNullIsKeywordAsConstDNFType */ null|(C&D) NAME_NULL = SOME_CONST; + const /* testSelfIsKeywordAsConstDNFType */ (self&B)|int NAME_SELF = SOME_CONST; + const bool|(A&/* testParentIsKeywordAsConstDNFType */ parent) NAME_PARENT = SOME_CONST; + + readonly public (A&B)/* testFalseIsKeywordAsConstDNFType */ |false $false; + protected /* testTrueIsKeywordAsConstDNFType */ true|(A&B) $true = SOME_CONST; + static private (A&B)|/* testNullIsKeywordAsConstDNFType */ null|(C&D) $null = SOME_CONST; + var string|/* testSelfIsKeywordAsConstDNFType */ (self&Stringable) $self = SOME_CONST; + protected (A/* testParentIsKeywordAsConstDNFType */ &parent)|float $parent = SOME_CONST; + + public function DNFWithFalse( + /* testFalseIsKeywordAsParamDNFType */ + false|(A&B) $param + ) : (A&B)/* testFalseIsKeywordAsReturnDNFType */|false { + + $closure = static function ( + array/* testTrueIsKeywordAsParamDNFType */|true|(A&B) $param + ) : (A&B)/* testTrueIsKeywordAsReturnDNFType */|true {}; + } + + + public function DNFWithNull( + /* testNullIsKeywordAsParamDNFType */ + null|(A&B) $param + ) : (A&B)/* testNullIsKeywordAsReturnDNFType */|null|(C&D) {} + + public function DNFWithSelf( + /* testSelfIsKeywordAsParamDNFType */ + (self&B)|mixed $param + ) : (A&B)/* testSelfIsKeywordAsReturnDNFType */|self { + + $arrow = fn ( + array|(Iterable /* testParentIsKeywordAsParamDNFType */&parent) $param + /* testParentIsKeywordAsReturnDNFType */ + ) : parent|(A&B) => $param->get(); + } +} diff --git a/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.php index f41db7305d..1b8d19881d 100644 --- a/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.php @@ -627,6 +627,90 @@ public static function dataKeywords() 'testMarker' => '/* testParentIsKeywordAsReturnIntersectionTypeLast */', 'expectedTokenType' => 'T_PARENT', ], + + 'false: DNF type in OO constant declaration' => [ + 'testMarker' => '/* testFalseIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_FALSE', + ], + 'true: DNF type in OO constant declaration' => [ + 'testMarker' => '/* testTrueIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_TRUE', + ], + 'null: DNF type in OO constant declaration' => [ + 'testMarker' => '/* testNullIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_NULL', + ], + 'self: DNF type in OO constant declaration' => [ + 'testMarker' => '/* testSelfIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_SELF', + ], + 'parent: DNF type in OO constant declaration' => [ + 'testMarker' => '/* testParentIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_PARENT', + ], + + 'false: DNF type in property declaration' => [ + 'testMarker' => '/* testFalseIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_FALSE', + ], + 'true: DNF type in property declaration' => [ + 'testMarker' => '/* testTrueIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_TRUE', + ], + 'null: DNF type in property declaration' => [ + 'testMarker' => '/* testNullIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_NULL', + ], + 'self: DNF type in property declaration' => [ + 'testMarker' => '/* testSelfIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_SELF', + ], + 'parent: DNF type in property declaration' => [ + 'testMarker' => '/* testParentIsKeywordAsConstDNFType */', + 'expectedTokenType' => 'T_PARENT', + ], + + 'false: DNF type in function param declaration' => [ + 'testMarker' => '/* testFalseIsKeywordAsParamDNFType */', + 'expectedTokenType' => 'T_FALSE', + ], + 'false: DNF type in function return declaration' => [ + 'testMarker' => '/* testFalseIsKeywordAsReturnDNFType */', + 'expectedTokenType' => 'T_FALSE', + ], + 'true: DNF type in function param declaration' => [ + 'testMarker' => '/* testTrueIsKeywordAsParamDNFType */', + 'expectedTokenType' => 'T_TRUE', + ], + 'true: DNF type in function return declaration' => [ + 'testMarker' => '/* testTrueIsKeywordAsReturnDNFType */', + 'expectedTokenType' => 'T_TRUE', + ], + 'null: DNF type in function param declaration' => [ + 'testMarker' => '/* testNullIsKeywordAsParamDNFType */', + 'expectedTokenType' => 'T_NULL', + ], + 'null: DNF type in function return declaration' => [ + 'testMarker' => '/* testNullIsKeywordAsReturnDNFType */', + 'expectedTokenType' => 'T_NULL', + ], + 'self: DNF type in function param declaration' => [ + 'testMarker' => '/* testSelfIsKeywordAsParamDNFType */', + 'expectedTokenType' => 'T_SELF', + ], + 'self: DNF type in function return declaration' => [ + 'testMarker' => '/* testSelfIsKeywordAsReturnDNFType */', + 'expectedTokenType' => 'T_SELF', + ], + 'parent: DNF type in function param declaration' => [ + 'testMarker' => '/* testParentIsKeywordAsParamDNFType */', + 'expectedTokenType' => 'T_PARENT', + ], + 'parent: DNF type in function return declaration' => [ + 'testMarker' => '/* testParentIsKeywordAsReturnDNFType */', + 'expectedTokenType' => 'T_PARENT', + ], + ]; }//end dataKeywords() diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.inc b/tests/Core/Tokenizer/TypeIntersectionTest.inc index d9a68db363..53177a5317 100644 --- a/tests/Core/Tokenizer/TypeIntersectionTest.inc +++ b/tests/Core/Tokenizer/TypeIntersectionTest.inc @@ -109,6 +109,20 @@ function globalFunctionWithSpreadAndReference( Foo&Bar ...$paramB ) {} + +$dnfTypes = new class { + /* testTypeIntersectionConstantTypeUnionBeforeDNF */ + const Foo|(A&B) UNION_BEFORE = /* testBitwiseAndOOConstDefaultValueDNF */ Foo|(A&B); + + /* testTypeIntersectionPropertyTypeUnionAfterDNF */ + protected (\FQN&namespace\Relative)|Partially\Qualified $union_after = /* testBitwiseAndPropertyDefaultValueDNF */ (A&B)|Foo; + + public function unionBeforeAndAfter( + /* testTypeIntersectionParamUnionBeforeAndAfterDNF */ + string|(Stringable&\Countable)|int $param = /* testBitwiseAndParamDefaultValueDNF */ ( CONST_A & CONST_B) | CONST_C + ): /* testTypeIntersectionReturnTypeUnionAfterDNF */ (A&B)|null {} +}; + /* testTypeIntersectionClosureParamIllegalNullable */ $closureWithParamType = function (?Foo&Bar $string) {}; diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.php b/tests/Core/Tokenizer/TypeIntersectionTest.php index c719850c19..bcbc8918cb 100644 --- a/tests/Core/Tokenizer/TypeIntersectionTest.php +++ b/tests/Core/Tokenizer/TypeIntersectionTest.php @@ -46,24 +46,27 @@ public function testBitwiseAnd($testMarker) 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 */'], - 'in return statement' => ['/* testBitwiseAnd4 */'], - 'reference for function parameter' => ['/* testBitwiseAnd5 */'], - 'in closure parameter default value' => ['/* testBitwiseAndClosureParamDefault */'], - 'in arrow function parameter default value' => ['/* testBitwiseAndArrowParamDefault */'], - 'in arrow function return expression' => ['/* testBitwiseAndArrowExpression */'], - 'in long array key' => ['/* testBitwiseAndInArrayKey */'], - 'in long array value' => ['/* testBitwiseAndInArrayValue */'], - 'in short array key' => ['/* testBitwiseAndInShortArrayKey */'], - 'in short array value' => ['/* testBitwiseAndInShortArrayValue */'], - 'in parameter in function call' => ['/* testBitwiseAndNonArrowFnFunctionCall */'], - 'function return by reference' => ['/* testBitwiseAnd6 */'], - 'live coding / undetermined' => ['/* testLiveCoding */'], + '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 */'], + 'in return statement' => ['/* testBitwiseAnd4 */'], + 'reference for function parameter' => ['/* testBitwiseAnd5 */'], + 'in OO constant default value DNF-like' => ['/* testBitwiseAndOOConstDefaultValueDNF */'], + 'in property default value DNF-like' => ['/* testBitwiseAndPropertyDefaultValueDNF */'], + 'in method parameter default value DNF-like' => ['/* testBitwiseAndParamDefaultValueDNF */'], + 'in closure parameter default value' => ['/* testBitwiseAndClosureParamDefault */'], + 'in arrow function parameter default value' => ['/* testBitwiseAndArrowParamDefault */'], + 'in arrow function return expression' => ['/* testBitwiseAndArrowExpression */'], + 'in long array key' => ['/* testBitwiseAndInArrayKey */'], + 'in long array value' => ['/* testBitwiseAndInArrayValue */'], + 'in short array key' => ['/* testBitwiseAndInShortArrayKey */'], + 'in short array value' => ['/* testBitwiseAndInShortArrayValue */'], + 'in parameter in function call' => ['/* testBitwiseAndNonArrowFnFunctionCall */'], + 'function return by reference' => ['/* testBitwiseAnd6 */'], + 'live coding / undetermined' => ['/* testLiveCoding */'], ]; }//end dataBitwiseAnd() @@ -134,6 +137,10 @@ public static function dataTypeIntersection() 'return type for method with fully qualified names' => ['/* testTypeIntersectionReturnFullyQualified */'], 'type for function parameter with reference' => ['/* testTypeIntersectionWithReference */'], 'type for function parameter with spread operator' => ['/* testTypeIntersectionWithSpreadOperator */'], + 'DNF type for OO constant, union before DNF' => ['/* testTypeIntersectionConstantTypeUnionBeforeDNF */'], + 'DNF type for property, union after DNF' => ['/* testTypeIntersectionPropertyTypeUnionAfterDNF */'], + 'DNF type for function param, union before and after DNF' => ['/* testTypeIntersectionParamUnionBeforeAndAfterDNF */'], + 'DNF type for function return, union after DNF with null' => ['/* testTypeIntersectionReturnTypeUnionAfterDNF */'], 'type for closure parameter with illegal nullable' => ['/* testTypeIntersectionClosureParamIllegalNullable */'], 'return type for closure' => ['/* testTypeIntersectionClosureReturn */'], 'type for arrow function parameter' => ['/* testTypeIntersectionArrowParam */'], diff --git a/tests/Core/Tokenizer/TypedConstantsTest.inc b/tests/Core/Tokenizer/TypedConstantsTest.inc index a68831392c..4c6212b78e 100644 --- a/tests/Core/Tokenizer/TypedConstantsTest.inc +++ b/tests/Core/Tokenizer/TypedConstantsTest.inc @@ -130,3 +130,27 @@ enum EnumWithIntersectionTypedConstants { /* testEnumConstTypedIntersectFullyQualifiedPartiallyQualified */ const \Fully\Qualified&Partially\Qualified UNION_FQN_PARTIAL = new Partial\Qualified; } + +$anonClassWithDNFTypes = new class() extends Something { + /* testAnonClassConstDNFTypeNullAfter */ + const (A&B)|null DNF_OR_NULL_1 = null; + /* testAnonClassConstDNFTypeNullBefore */ + public final const NULL|(A&B) DNF_OR_NULL_2 = null; + /* testAnonClassConstDNFTypeFalseBefore */ + final const false|(C&D) DNF_OR_FALSE = false; + /* testAnonClassConstDNFTypeTrueAfter */ + private final const ( F & G ) | true DNF_OR_ARRAY = true; + /* testAnonClassConstDNFTypeTrueBeforeFalseAfter */ + public const TRUE|(SplBool&Stringable)|FALSE DNF_OR_BOOL = true; + /* testAnonClassConstDNFTypeArrayAfter */ + final protected const (Traversable&Countable)|array DNF_OR_ARRAY_1 = []; + /* testAnonClassConstDNFTypeArrayBefore */ + private const array /*comment*/ | ( Traversable /*comment*/ & Countable ) DNF_OR_ARRAY_2 = new MyClass; + /* testAnonClassConstDNFTypeInvalidNullable */ + const ? (Invalid&Fatal)|NullableNotAllowed DNF = null; + + /* testAnonClassConstDNFTypeFQNRelativePartiallyQualified */ + const (\FQN&namespace\Relative)|Partially\Qualified DNF_CLASSNAME = MyClass::getInstance(); + /* testAnonClassConstDNFTypeParentSelfStatic */ + const (parent&self)|static DNF_PARENT = parent::getInstance(); +}; diff --git a/tests/Core/Tokenizer/TypedConstantsTest.php b/tests/Core/Tokenizer/TypedConstantsTest.php index 0c4a9b6146..750f02d4c6 100644 --- a/tests/Core/Tokenizer/TypedConstantsTest.php +++ b/tests/Core/Tokenizer/TypedConstantsTest.php @@ -116,7 +116,9 @@ public static function dataUntypedConstant() * @dataProvider dataNullableTypedConstant * @dataProvider dataUnionTypedConstant * @dataProvider dataIntersectionTypedConstant + * @dataProvider dataDNFTypedConstant * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ @@ -512,4 +514,154 @@ public static function dataIntersectionTypedConstant() }//end dataIntersectionTypedConstant() + /** + * Data provider. + * + * @see testTypedConstant() + * + * @return array> + */ + public static function dataDNFTypedConstant() + { + $data = [ + 'DNF type: null after' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeNullAfter */', + 'sequence' => [ + T_TYPE_OPEN_PARENTHESIS, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + T_TYPE_UNION, + T_NULL, + ], + ], + 'DNF type: null before' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeNullBefore */', + 'sequence' => [ + T_NULL, + T_TYPE_UNION, + T_TYPE_OPEN_PARENTHESIS, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + ], + ], + 'DNF type: false before' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeFalseBefore */', + 'sequence' => [ + T_FALSE, + T_TYPE_UNION, + T_TYPE_OPEN_PARENTHESIS, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + ], + ], + 'DNF type: true after' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeTrueAfter */', + 'sequence' => [ + T_TYPE_OPEN_PARENTHESIS, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + T_TYPE_UNION, + T_TRUE, + ], + ], + 'DNF type: true before, false after' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeTrueBeforeFalseAfter */', + 'sequence' => [ + T_TRUE, + T_TYPE_UNION, + T_TYPE_OPEN_PARENTHESIS, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + T_TYPE_UNION, + T_FALSE, + ], + ], + 'DNF type: array after' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeArrayAfter */', + 'sequence' => [ + T_TYPE_OPEN_PARENTHESIS, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + T_TYPE_UNION, + T_STRING, + ], + ], + 'DNF type: array before' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeArrayBefore */', + 'sequence' => [ + T_STRING, + T_TYPE_UNION, + T_TYPE_OPEN_PARENTHESIS, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + ], + ], + 'DNF type: invalid nullable DNF' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeInvalidNullable */', + 'sequence' => [ + T_NULLABLE, + T_TYPE_OPEN_PARENTHESIS, + T_STRING, + T_TYPE_INTERSECTION, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + T_TYPE_UNION, + T_STRING, + ], + ], + 'DNF type: FQN/namespace relative/partially qualified names' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeFQNRelativePartiallyQualified */', + 'sequence' => [ + T_TYPE_OPEN_PARENTHESIS, + T_NS_SEPARATOR, + T_STRING, + T_TYPE_INTERSECTION, + T_NAMESPACE, + T_NS_SEPARATOR, + T_STRING, + T_TYPE_CLOSE_PARENTHESIS, + T_TYPE_UNION, + T_STRING, + T_NS_SEPARATOR, + T_STRING, + ], + ], + 'DNF type: invalid self/parent/static' => [ + 'testMarker' => '/* testAnonClassConstDNFTypeParentSelfStatic */', + 'sequence' => [ + T_TYPE_OPEN_PARENTHESIS, + T_PARENT, + T_TYPE_INTERSECTION, + T_SELF, + T_TYPE_CLOSE_PARENTHESIS, + T_TYPE_UNION, + 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 dataDNFTypedConstant() + + }//end class