diff --git a/package.xml b/package.xml
index d4c2aa4b3e..1dfb29dba5 100644
--- a/package.xml
+++ b/package.xml
@@ -126,6 +126,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
@@ -2006,6 +2008,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
@@ -2071,6 +2075,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
diff --git a/src/Files/File.php b/src/Files/File.php
index d1e9a4da87..eac30ef95c 100644
--- a/src/Files/File.php
+++ b/src/Files/File.php
@@ -1288,7 +1288,8 @@ public function getDeclarationName($stackPtr)
* // or FALSE if there is no type hint.
* 'type_hint_end_token' => integer, // The stack pointer to the end of the type hint
* // or FALSE if there is no type hint.
- * 'nullable_type' => boolean, // TRUE if the var type is nullable.
+ * 'nullable_type' => boolean, // TRUE if the type is preceded by the nullability
+ * // operator.
* 'comma_token' => integer, // The stack pointer to the comma after the param
* // or FALSE if this is the last param.
* )
@@ -1443,6 +1444,9 @@ public function getMethodParameters($stackPtr)
break;
case T_NAMESPACE:
case T_NS_SEPARATOR:
+ case T_TYPE_UNION:
+ case T_FALSE:
+ case T_NULL:
// Part of a type hint or default value.
if ($defaultStart === null) {
if ($typeHintToken === false) {
@@ -1533,7 +1537,8 @@ public function getMethodParameters($stackPtr)
* 'return_type' => '', // The return type of the method.
* 'return_type_token' => integer, // The stack pointer to the start of the return type
* // or FALSE if there is no return type.
- * 'nullable_return_type' => false, // TRUE if the return type is nullable.
+ * 'nullable_return_type' => false, // TRUE if the return type is preceded by the
+ * // nullability operator.
* 'is_abstract' => false, // TRUE if the abstract keyword was found.
* 'is_final' => false, // TRUE if the final keyword was found.
* 'is_static' => false, // TRUE if the static keyword was found.
@@ -1631,8 +1636,11 @@ public function getMethodProperties($stackPtr)
T_SELF => T_SELF,
T_PARENT => T_PARENT,
T_STATIC => T_STATIC,
+ T_FALSE => T_FALSE,
+ T_NULL => T_NULL,
T_NAMESPACE => T_NAMESPACE,
T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_TYPE_UNION => T_TYPE_UNION,
];
for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
@@ -1700,7 +1708,8 @@ public function getMethodProperties($stackPtr)
* // or FALSE if there is no type.
* 'type_end_token' => integer, // The stack pointer to the end of the type
* // or FALSE if there is no type.
- * 'nullable_type' => boolean, // TRUE if the type is nullable.
+ * 'nullable_type' => boolean, // TRUE if the type is preceded by the nullability
+ * // operator.
* );
*
*
@@ -1815,8 +1824,11 @@ public function getMemberProperties($stackPtr)
T_CALLABLE => T_CALLABLE,
T_SELF => T_SELF,
T_PARENT => T_PARENT,
+ T_FALSE => T_FALSE,
+ T_NULL => T_NULL,
T_NAMESPACE => T_NAMESPACE,
T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_TYPE_UNION => T_TYPE_UNION,
];
for ($i; $i < $stackPtr; $i++) {
diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php
index 7aa484ef70..c0e202a759 100644
--- a/src/Tokenizers/PHP.php
+++ b/src/Tokenizers/PHP.php
@@ -439,6 +439,7 @@ class PHP extends Tokenizer
T_BACKTICK => 1,
T_OPEN_SHORT_ARRAY => 1,
T_CLOSE_SHORT_ARRAY => 1,
+ T_TYPE_UNION => 1,
];
/**
@@ -1475,6 +1476,7 @@ function return types. We want to keep the parenthesis map clean,
T_SELF => T_SELF,
T_PARENT => T_PARENT,
T_NAMESPACE => T_NAMESPACE,
+ T_STATIC => T_STATIC,
T_NS_SEPARATOR => T_NS_SEPARATOR,
];
@@ -1509,12 +1511,14 @@ function return types. We want to keep the parenthesis map clean,
}//end for
// Any T_ARRAY tokens we find between here and the next
- // token that can't be part of the return type need to be
+ // token that can't be part of the return type, need to be
// converted to T_STRING tokens.
for ($x; $x < $numTokens; $x++) {
- if (is_array($tokens[$x]) === false || isset($allowed[$tokens[$x][0]]) === false) {
+ if ((is_array($tokens[$x]) === false && $tokens[$x] !== '|')
+ || (is_array($tokens[$x]) === true && isset($allowed[$tokens[$x][0]]) === false)
+ ) {
break;
- } else if ($tokens[$x][0] === T_ARRAY) {
+ } else if (is_array($tokens[$x]) === true && $tokens[$x][0] === T_ARRAY) {
$tokens[$x][0] = T_STRING;
if (PHP_CODESNIFFER_VERBOSITY > 1) {
@@ -1996,6 +2000,7 @@ protected function processAdditional()
T_PARENT => T_PARENT,
T_SELF => T_SELF,
T_STATIC => T_STATIC,
+ T_TYPE_UNION => T_TYPE_UNION,
];
$closer = $this->tokens[$x]['parenthesis_closer'];
@@ -2176,6 +2181,177 @@ protected function processAdditional()
}
}
+ continue;
+ } else if ($this->tokens[$i]['code'] === T_BITWISE_OR) {
+ /*
+ Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR.
+ */
+
+ $allowed = [
+ T_STRING => T_STRING,
+ T_CALLABLE => T_CALLABLE,
+ T_SELF => T_SELF,
+ T_PARENT => T_PARENT,
+ T_STATIC => T_STATIC,
+ T_FALSE => T_FALSE,
+ T_NULL => T_NULL,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ ];
+
+ $suspectedType = null;
+ $typeTokenCount = 0;
+
+ for ($x = ($i + 1); $x < $numTokens; $x++) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
+ continue;
+ }
+
+ if (isset($allowed[$this->tokens[$x]['code']]) === true) {
+ ++$typeTokenCount;
+ continue;
+ }
+
+ if ($typeTokenCount > 0
+ && ($this->tokens[$x]['code'] === T_BITWISE_AND
+ || $this->tokens[$x]['code'] === T_ELLIPSIS)
+ ) {
+ // Skip past reference and variadic indicators for parameter types.
+ ++$x;
+ continue;
+ }
+
+ if ($this->tokens[$x]['code'] === T_VARIABLE) {
+ // Parameter/Property defaults can not contain variables, so this could be a type.
+ $suspectedType = 'property or parameter';
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] === T_DOUBLE_ARROW) {
+ // Possible arrow function.
+ $suspectedType = 'return';
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] === T_SEMICOLON) {
+ // Possible abstract method or interface method.
+ $suspectedType = 'return';
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] === T_OPEN_CURLY_BRACKET
+ && isset($this->tokens[$x]['scope_condition']) === true
+ && $this->tokens[$this->tokens[$x]['scope_condition']]['code'] === T_FUNCTION
+ ) {
+ $suspectedType = 'return';
+ }
+
+ break;
+ }//end for
+
+ if ($typeTokenCount === 0 || isset($suspectedType) === false) {
+ // Definitely not a union type, move on.
+ continue;
+ }
+
+ $typeTokenCount = 0;
+ $unionOperators = [$i];
+ $confirmed = false;
+
+ for ($x = ($i - 1); $x >= 0; $x--) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
+ continue;
+ }
+
+ if (isset($allowed[$this->tokens[$x]['code']]) === true) {
+ ++$typeTokenCount;
+ continue;
+ }
+
+ // Union types can't use the nullable operator, but be tolerant to parse errors.
+ if ($typeTokenCount > 0 && $this->tokens[$x]['code'] === T_NULLABLE) {
+ continue;
+ }
+
+ if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
+ $unionOperators[] = $x;
+ continue;
+ }
+
+ if ($suspectedType === 'return' && $this->tokens[$x]['code'] === T_COLON) {
+ $confirmed = true;
+ break;
+ }
+
+ if ($suspectedType === 'property or parameter'
+ && (isset(Util\Tokens::$scopeModifiers[$this->tokens[$x]['code']]) === true
+ || $this->tokens[$x]['code'] === T_VAR)
+ ) {
+ // This will also confirm constructor property promotion parameters, but that's fine.
+ $confirmed = true;
+ }
+
+ break;
+ }//end for
+
+ if ($confirmed === false
+ && $suspectedType === 'property or parameter'
+ && isset($this->tokens[$i]['nested_parenthesis']) === true
+ ) {
+ $parens = $this->tokens[$i]['nested_parenthesis'];
+ $last = end($parens);
+
+ if (isset($this->tokens[$last]['parenthesis_owner']) === true
+ && $this->tokens[$this->tokens[$last]['parenthesis_owner']]['code'] === T_FUNCTION
+ ) {
+ $confirmed = true;
+ } else {
+ // No parenthesis owner set, this may be an arrow function which has not yet
+ // had additional processing done.
+ if (isset($this->tokens[$last]['parenthesis_opener']) === true) {
+ for ($x = ($this->tokens[$last]['parenthesis_opener'] - 1); $x >= 0; $x--) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] === T_FN) {
+ for (--$x; $x >= 0; $x--) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true
+ || $this->tokens[$x]['code'] === T_BITWISE_AND
+ ) {
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] !== T_FUNCTION) {
+ $confirmed = true;
+ }
+ }
+ }//end if
+ }//end if
+
+ unset($parens, $last);
+ }//end if
+
+ if ($confirmed === false) {
+ // Not a union type after all, move on.
+ continue;
+ }
+
+ foreach ($unionOperators as $x) {
+ $this->tokens[$x]['code'] = T_TYPE_UNION;
+ $this->tokens[$x]['type'] = 'T_TYPE_UNION';
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $line = $this->tokens[$x]['line'];
+ echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL;
+ }
+ }
+
continue;
} else if ($this->tokens[$i]['code'] === T_STATIC) {
for ($x = ($i - 1); $x > 0; $x--) {
diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php
index 952ccc2c12..aee035d352 100644
--- a/src/Util/Tokens.php
+++ b/src/Util/Tokens.php
@@ -75,6 +75,7 @@
define('T_ZSR', 'PHPCS_T_ZSR');
define('T_ZSR_EQUAL', 'PHPCS_T_ZSR_EQUAL');
define('T_FN_ARROW', 'T_FN_ARROW');
+define('T_TYPE_UNION', 'T_TYPE_UNION');
// Some PHP 5.5 tokens, replicated for lower versions.
if (defined('T_FINALLY') === false) {
diff --git a/tests/Core/File/GetMemberPropertiesTest.inc b/tests/Core/File/GetMemberPropertiesTest.inc
index 0a511e588d..9cc56bd53a 100644
--- a/tests/Core/File/GetMemberPropertiesTest.inc
+++ b/tests/Core/File/GetMemberPropertiesTest.inc
@@ -193,3 +193,50 @@ class NSOperatorInType {
/* testNamespaceOperatorTypeHint */
public ?namespace\Name $prop;
}
+
+$anon = class() {
+ /* testPHP8UnionTypesSimple */
+ public int|float $unionTypeSimple;
+
+ /* testPHP8UnionTypesTwoClasses */
+ private MyClassA|\Package\MyClassB $unionTypesTwoClasses;
+
+ /* testPHP8UnionTypesAllBaseTypes */
+ protected array|bool|int|float|NULL|object|string $unionTypesAllBaseTypes;
+
+ /* testPHP8UnionTypesAllPseudoTypes */
+ // Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method.
+ var false|mixed|self|parent|iterable|Resource $unionTypesAllPseudoTypes;
+
+ /* testPHP8UnionTypesIllegalTypes */
+ // Intentional fatal error - types which are not allowed for properties, but that's not the concern of the method.
+ public callable|static|void $unionTypesIllegalTypes;
+
+ /* testPHP8UnionTypesNullable */
+ // Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method.
+ public ?int|float $unionTypesNullable;
+
+ /* testPHP8PseudoTypeNull */
+ // Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method.
+ public null $pseudoTypeNull;
+
+ /* testPHP8PseudoTypeFalse */
+ // Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method.
+ public false $pseudoTypeFalse;
+
+ /* testPHP8PseudoTypeFalseAndBool */
+ // Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method.
+ public bool|FALSE $pseudoTypeFalseAndBool;
+
+ /* testPHP8ObjectAndClass */
+ // Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method.
+ public object|ClassName $objectAndClass;
+
+ /* testPHP8PseudoTypeIterableAndArray */
+ // Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method.
+ public iterable|array|Traversable $pseudoTypeIterableAndArray;
+
+ /* testPHP8DuplicateTypeInUnionWhitespaceAndComment */
+ // Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method.
+ public int |string| /*comment*/ INT $duplicateTypeInUnion;
+};
diff --git a/tests/Core/File/GetMemberPropertiesTest.php b/tests/Core/File/GetMemberPropertiesTest.php
index dfee43e203..a59effc752 100644
--- a/tests/Core/File/GetMemberPropertiesTest.php
+++ b/tests/Core/File/GetMemberPropertiesTest.php
@@ -489,6 +489,127 @@ public function dataGetMemberProperties()
'nullable_type' => true,
],
],
+ [
+ '/* testPHP8UnionTypesSimple */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'int|float',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesTwoClasses */',
+ [
+ 'scope' => 'private',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'MyClassA|\Package\MyClassB',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesAllBaseTypes */',
+ [
+ 'scope' => 'protected',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'array|bool|int|float|NULL|object|string',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesAllPseudoTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'is_static' => false,
+ 'type' => 'false|mixed|self|parent|iterable|Resource',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesIllegalTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ // Missing static, but that's OK as not an allowed syntax.
+ 'type' => 'callable||void',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesNullable */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => '?int|float',
+ 'nullable_type' => true,
+ ],
+ ],
+ [
+ '/* testPHP8PseudoTypeNull */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'null',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8PseudoTypeFalse */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'false',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8PseudoTypeFalseAndBool */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'bool|FALSE',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8ObjectAndClass */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'object|ClassName',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8PseudoTypeIterableAndArray */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'iterable|array|Traversable',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'int|string|INT',
+ 'nullable_type' => false,
+ ],
+ ],
];
}//end dataGetMemberProperties()
diff --git a/tests/Core/File/GetMethodParametersTest.inc b/tests/Core/File/GetMethodParametersTest.inc
index 4ffd44221f..56fa6eae41 100644
--- a/tests/Core/File/GetMethodParametersTest.inc
+++ b/tests/Core/File/GetMethodParametersTest.inc
@@ -41,3 +41,47 @@ function mixedTypeHintNullable(?Mixed $var1) {}
/* testNamespaceOperatorTypeHint */
function namespaceOperatorTypeHint(?namespace\Name $var1) {}
+
+/* testPHP8UnionTypesSimple */
+function unionTypeSimple(int|float $number, self|parent &...$obj) {}
+
+/* testPHP8UnionTypesSimpleWithBitwiseOrInDefault */
+$fn = fn(int|float $var = CONSTANT_A | CONSTANT_B) => $var;
+
+/* testPHP8UnionTypesTwoClasses */
+function unionTypesTwoClasses(MyClassA|\Package\MyClassB $var) {}
+
+/* testPHP8UnionTypesAllBaseTypes */
+function unionTypesAllBaseTypes(array|bool|callable|int|float|null|object|string $var) {}
+
+/* testPHP8UnionTypesAllPseudoTypes */
+// Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method.
+function unionTypesAllPseudoTypes(false|mixed|self|parent|iterable|Resource $var) {}
+
+/* testPHP8UnionTypesNullable */
+// Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method.
+$closure = function (?int|float $number) {};
+
+/* testPHP8PseudoTypeNull */
+// Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method.
+function pseudoTypeNull(null $var = null) {}
+
+/* testPHP8PseudoTypeFalse */
+// Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method.
+function pseudoTypeFalse(false $var = false) {}
+
+/* testPHP8PseudoTypeFalseAndBool */
+// Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method.
+function pseudoTypeFalseAndBool(bool|false $var = false) {}
+
+/* testPHP8ObjectAndClass */
+// Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method.
+function objectAndClass(object|ClassName $var) {}
+
+/* testPHP8PseudoTypeIterableAndArray */
+// Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method.
+function pseudoTypeIterableAndArray(iterable|array|Traversable $var) {}
+
+/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */
+// Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method.
+function duplicateTypeInUnion( int | string /*comment*/ | INT $var) {}
diff --git a/tests/Core/File/GetMethodParametersTest.php b/tests/Core/File/GetMethodParametersTest.php
index 92f51959e6..ad4cf8b67e 100644
--- a/tests/Core/File/GetMethodParametersTest.php
+++ b/tests/Core/File/GetMethodParametersTest.php
@@ -340,6 +340,282 @@ public function testNamespaceOperatorTypeHint()
}//end testNamespaceOperatorTypeHint()
+ /**
+ * Verify recognition of PHP8 union type declaration.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesSimple()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$number',
+ 'content' => 'int|float $number',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'int|float',
+ 'nullable_type' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$obj',
+ 'content' => 'self|parent &...$obj',
+ 'pass_by_reference' => true,
+ 'variable_length' => true,
+ 'type_hint' => 'self|parent',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesSimple()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with a bitwise or in the default value.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesSimpleWithBitwiseOrInDefault()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'int|float $var = CONSTANT_A | CONSTANT_B',
+ 'default' => 'CONSTANT_A | CONSTANT_B',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'int|float',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesSimpleWithBitwiseOrInDefault()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with two classes.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesTwoClasses()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'MyClassA|\Package\MyClassB $var',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'MyClassA|\Package\MyClassB',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesTwoClasses()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with all base types.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesAllBaseTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'array|bool|callable|int|float|null|object|string $var',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'array|bool|callable|int|float|null|object|string',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesAllBaseTypes()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with all pseudo types.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesAllPseudoTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'false|mixed|self|parent|iterable|Resource $var',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'false|mixed|self|parent|iterable|Resource',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesAllPseudoTypes()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesNullable()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$number',
+ 'content' => '?int|float $number',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?int|float',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesNullable()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) single type null.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeNull()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'null $var = null',
+ 'default' => 'null',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'null',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeNull()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) single type false.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeFalse()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'false $var = false',
+ 'default' => 'false',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'false',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeFalse()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type false combined with type bool.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeFalseAndBool()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'bool|false $var = false',
+ 'default' => 'false',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'bool|false',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeFalseAndBool()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type object combined with a class name.
+ *
+ * @return void
+ */
+ public function testPHP8ObjectAndClass()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'object|ClassName $var',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'object|ClassName',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ObjectAndClass()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type iterable combined with array/Traversable.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeIterableAndArray()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'iterable|array|Traversable $var',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'iterable|array|Traversable',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeIterableAndArray()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) duplicate types.
+ *
+ * @return void
+ */
+ public function testPHP8DuplicateTypeInUnionWhitespaceAndComment()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'int | string /*comment*/ | INT $var',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'int|string|INT',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8DuplicateTypeInUnionWhitespaceAndComment()
+
+
/**
* Test helper.
*
@@ -350,7 +626,7 @@ public function testNamespaceOperatorTypeHint()
*/
private function getMethodParametersTestHelper($commentString, $expected)
{
- $function = $this->getTargetToken($commentString, [T_FUNCTION, T_FN]);
+ $function = $this->getTargetToken($commentString, [T_FUNCTION, T_CLOSURE, T_FN]);
$found = self::$phpcsFile->getMethodParameters($function);
$this->assertArraySubset($expected, $found, true);
diff --git a/tests/Core/File/GetMethodPropertiesTest.inc b/tests/Core/File/GetMethodPropertiesTest.inc
index 82c032efa0..3e9682df6e 100644
--- a/tests/Core/File/GetMethodPropertiesTest.inc
+++ b/tests/Core/File/GetMethodPropertiesTest.inc
@@ -83,3 +83,46 @@ function mixedTypeHintNullable(): ?mixed {}
/* testNamespaceOperatorTypeHint */
function namespaceOperatorTypeHint() : ?namespace\Name {}
+
+/* testPHP8UnionTypesSimple */
+function unionTypeSimple($number) : int|float {}
+
+/* testPHP8UnionTypesTwoClasses */
+$fn = fn($var): MyClassA|\Package\MyClassB => $var;
+
+/* testPHP8UnionTypesAllBaseTypes */
+function unionTypesAllBaseTypes() : array|bool|callable|int|float|null|Object|string {}
+
+/* testPHP8UnionTypesAllPseudoTypes */
+// Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method.
+function unionTypesAllPseudoTypes($var) : false|MIXED|self|parent|static|iterable|Resource|void {}
+
+/* testPHP8UnionTypesNullable */
+// Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method.
+$closure = function () use($a) :?int|float {};
+
+/* testPHP8PseudoTypeNull */
+// Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method.
+function pseudoTypeNull(): null {}
+
+/* testPHP8PseudoTypeFalse */
+// Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method.
+function pseudoTypeFalse(): false {}
+
+/* testPHP8PseudoTypeFalseAndBool */
+// Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method.
+function pseudoTypeFalseAndBool(): bool|false {}
+
+/* testPHP8ObjectAndClass */
+// Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method.
+function objectAndClass(): object|ClassName {}
+
+/* testPHP8PseudoTypeIterableAndArray */
+// Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method.
+interface FooBar {
+ public function pseudoTypeIterableAndArray(): iterable|array|Traversable;
+}
+
+/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */
+// Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method.
+function duplicateTypeInUnion(): int | /*comment*/ string | INT {}
diff --git a/tests/Core/File/GetMethodPropertiesTest.php b/tests/Core/File/GetMethodPropertiesTest.php
index ee11fc2b9f..d912d6b1ce 100644
--- a/tests/Core/File/GetMethodPropertiesTest.php
+++ b/tests/Core/File/GetMethodPropertiesTest.php
@@ -475,6 +475,259 @@ public function testNamespaceOperatorTypeHint()
}//end testNamespaceOperatorTypeHint()
+ /**
+ * Verify recognition of PHP8 union type declaration.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesSimple()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'int|float',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesSimple()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with two classes.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesTwoClasses()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'MyClassA|\Package\MyClassB',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesTwoClasses()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with all base types.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesAllBaseTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'array|bool|callable|int|float|null|Object|string',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesAllBaseTypes()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with all pseudo types.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesAllPseudoTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'false|MIXED|self|parent|static|iterable|Resource|void',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesAllPseudoTypes()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesNullable()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => '?int|float',
+ 'nullable_return_type' => true,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesNullable()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) single type null.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeNull()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'null',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeNull()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) single type false.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeFalse()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'false',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeFalse()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type false combined with type bool.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeFalseAndBool()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'bool|false',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeFalseAndBool()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type object combined with a class name.
+ *
+ * @return void
+ */
+ public function testPHP8ObjectAndClass()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'object|ClassName',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ObjectAndClass()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type iterable combined with array/Traversable.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeIterableAndArray()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'return_type' => 'iterable|array|Traversable',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => false,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeIterableAndArray()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) duplicate types.
+ *
+ * @return void
+ */
+ public function testPHP8DuplicateTypeInUnionWhitespaceAndComment()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'int|string|INT',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8DuplicateTypeInUnionWhitespaceAndComment()
+
+
/**
* Test helper.
*
diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.inc b/tests/Core/Tokenizer/BackfillFnTokenTest.inc
index 46c165a2d2..f470bb0c67 100644
--- a/tests/Core/Tokenizer/BackfillFnTokenTest.inc
+++ b/tests/Core/Tokenizer/BackfillFnTokenTest.inc
@@ -81,6 +81,12 @@ fn(array $a) : array => $a;
/* testStaticReturnType */
fn(array $a) : static => $a;
+/* testUnionParamType */
+$arrowWithUnionParam = fn(int|float $param) : SomeClass => new SomeClass($param);
+
+/* testUnionReturnType */
+$arrowWithUnionReturn = fn($param) : int|float => $param | 10;
+
/* testTernary */
$fn = fn($a) => $a ? /* testTernaryThen */ fn() : string => 'a' : /* testTernaryElse */ fn() : string => 'b';
@@ -130,6 +136,9 @@ $a = MyNS\Sub\Fn($param);
/* testNonArrowNamespaceOperatorFunctionCall */
$a = namespace\fn($param);
+/* testNonArrowFunctionNameWithUnionTypes */
+function fn(int|float $param) : string|null {}
+
/* testLiveCoding */
// Intentional parse error. This has to be the last test in the file.
$fn = fn
diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.php b/tests/Core/Tokenizer/BackfillFnTokenTest.php
index 9ef8dad442..b8ea0170ee 100644
--- a/tests/Core/Tokenizer/BackfillFnTokenTest.php
+++ b/tests/Core/Tokenizer/BackfillFnTokenTest.php
@@ -531,6 +531,62 @@ public function testKeywordReturnTypes()
}//end testKeywordReturnTypes()
+ /**
+ * Test arrow function with a union parameter type.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testUnionParamType()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken('/* testUnionParamType */', T_FN);
+ $this->backfillHelper($token);
+
+ $this->assertSame($tokens[$token]['scope_opener'], ($token + 13), 'Scope opener is not the arrow token');
+ $this->assertSame($tokens[$token]['scope_closer'], ($token + 21), 'Scope closer is not the semicolon token');
+
+ $opener = $tokens[$token]['scope_opener'];
+ $this->assertSame($tokens[$opener]['scope_opener'], ($token + 13), 'Opener scope opener is not the arrow token');
+ $this->assertSame($tokens[$opener]['scope_closer'], ($token + 21), 'Opener scope closer is not the semicolon token');
+
+ $closer = $tokens[$token]['scope_closer'];
+ $this->assertSame($tokens[$closer]['scope_opener'], ($token + 13), 'Closer scope opener is not the arrow token');
+ $this->assertSame($tokens[$closer]['scope_closer'], ($token + 21), 'Closer scope closer is not the semicolon token');
+
+ }//end testUnionParamType()
+
+
+ /**
+ * Test arrow function with a union return type.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testUnionReturnType()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken('/* testUnionReturnType */', T_FN);
+ $this->backfillHelper($token);
+
+ $this->assertSame($tokens[$token]['scope_opener'], ($token + 11), 'Scope opener is not the arrow token');
+ $this->assertSame($tokens[$token]['scope_closer'], ($token + 18), 'Scope closer is not the semicolon token');
+
+ $opener = $tokens[$token]['scope_opener'];
+ $this->assertSame($tokens[$opener]['scope_opener'], ($token + 11), 'Opener scope opener is not the arrow token');
+ $this->assertSame($tokens[$opener]['scope_closer'], ($token + 18), 'Opener scope closer is not the semicolon token');
+
+ $closer = $tokens[$token]['scope_closer'];
+ $this->assertSame($tokens[$closer]['scope_opener'], ($token + 11), 'Closer scope opener is not the arrow token');
+ $this->assertSame($tokens[$closer]['scope_closer'], ($token + 18), 'Closer scope closer is not the semicolon token');
+
+ }//end testUnionReturnType()
+
+
/**
* Test arrow functions used in ternary operators.
*
@@ -690,6 +746,7 @@ public function dataNotAnArrowFunction()
'Fn',
],
['/* testNonArrowNamespaceOperatorFunctionCall */'],
+ ['/* testNonArrowFunctionNameWithUnionTypes */'],
['/* testLiveCoding */'],
];
diff --git a/tests/Core/Tokenizer/BitwiseOrTest.inc b/tests/Core/Tokenizer/BitwiseOrTest.inc
new file mode 100644
index 0000000000..921ef7f5ae
--- /dev/null
+++ b/tests/Core/Tokenizer/BitwiseOrTest.inc
@@ -0,0 +1,85 @@
+ $param | $int;
+
+/* testTypeUnionArrowReturnType */
+$arrowWithReturnType = fn ($param) : int|null => $param * 10;
+
+/* testBitwiseOrInArrayKey */
+$array = array(
+ A | B => /* testBitwiseOrInArrayValue */ B | C
+);
+
+/* testBitwiseOrInShortArrayKey */
+$array = [
+ A | B => /* testBitwiseOrInShortArrayValue */ B | C
+];
+
+/* testBitwiseOrTryCatch */
+try {
+} catch ( ExceptionA | ExceptionB $e ) {
+}
+
+/* testBitwiseOrNonArrowFnFunctionCall */
+$obj->fn($something | $else);
+
+/* testTypeUnionNonArrowFunctionDeclaration */
+function &fn(int|false $something) {}
+
+/* testLiveCoding */
+// Intentional parse error. This has to be the last test in the file.
+return function( type|
diff --git a/tests/Core/Tokenizer/BitwiseOrTest.php b/tests/Core/Tokenizer/BitwiseOrTest.php
new file mode 100644
index 0000000000..88d8e587a0
--- /dev/null
+++ b/tests/Core/Tokenizer/BitwiseOrTest.php
@@ -0,0 +1,122 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class BitwiseOrTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that non-union type bitwise or tokens are still tokenized as bitwise or.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataBitwiseOr
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testBitwiseOr($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_BITWISE_OR, T_TYPE_UNION]);
+ $this->assertSame(T_BITWISE_OR, $tokens[$opener]['code']);
+ $this->assertSame('T_BITWISE_OR', $tokens[$opener]['type']);
+
+ }//end testBitwiseOr()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testBitwiseOr()
+ *
+ * @return array
+ */
+ public function dataBitwiseOr()
+ {
+ return [
+ ['/* testBitwiseOr1 */'],
+ ['/* testBitwiseOr2 */'],
+ ['/* testBitwiseOrPropertyDefaultValue */'],
+ ['/* testBitwiseOrParamDefaultValue */'],
+ ['/* testBitwiseOr3 */'],
+ ['/* testBitwiseOrClosureParamDefault */'],
+ ['/* testBitwiseOrArrowParamDefault */'],
+ ['/* testBitwiseOrArrowExpression */'],
+ ['/* testBitwiseOrInArrayKey */'],
+ ['/* testBitwiseOrInArrayValue */'],
+ ['/* testBitwiseOrInShortArrayKey */'],
+ ['/* testBitwiseOrInShortArrayValue */'],
+ ['/* testBitwiseOrTryCatch */'],
+ ['/* testBitwiseOrNonArrowFnFunctionCall */'],
+ ['/* testLiveCoding */'],
+ ];
+
+ }//end dataBitwiseOr()
+
+
+ /**
+ * Test that bitwise or tokens when used as part of a union type are tokenized as `T_TYPE_UNION`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataTypeUnion
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testTypeUnion($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_BITWISE_OR, T_TYPE_UNION]);
+ $this->assertSame(T_TYPE_UNION, $tokens[$opener]['code']);
+ $this->assertSame('T_TYPE_UNION', $tokens[$opener]['type']);
+
+ }//end testTypeUnion()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testTypeUnion()
+ *
+ * @return array
+ */
+ public function dataTypeUnion()
+ {
+ return [
+ ['/* testTypeUnionPropertySimple */'],
+ ['/* testTypeUnionPropertyReverseModifierOrder */'],
+ ['/* testTypeUnionPropertyMulti1 */'],
+ ['/* testTypeUnionPropertyMulti2 */'],
+ ['/* testTypeUnionPropertyMulti3 */'],
+ ['/* testTypeUnionParam1 */'],
+ ['/* testTypeUnionParam2 */'],
+ ['/* testTypeUnionParam3 */'],
+ ['/* testTypeUnionReturnType */'],
+ ['/* testTypeUnionConstructorPropertyPromotion */'],
+ ['/* testTypeUnionAbstractMethodReturnType1 */'],
+ ['/* testTypeUnionAbstractMethodReturnType2 */'],
+ ['/* testTypeUnionClosureParamIllegalNullable */'],
+ ['/* testTypeUnionClosureReturn */'],
+ ['/* testTypeUnionArrowParam */'],
+ ['/* testTypeUnionArrowReturnType */'],
+ ['/* testTypeUnionNonArrowFunctionDeclaration */'],
+ ];
+
+ }//end dataTypeUnion()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php b/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php
index 27cd47783e..24667e486f 100644
--- a/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php
+++ b/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php
@@ -657,9 +657,8 @@ public function dataIdentifierTokenization()
'type' => 'T_STRING',
'content' => 'Name',
],
- // TODO: change this to T_TYPE_UNION when #3032 is merged.
[
- 'type' => 'T_BITWISE_OR',
+ 'type' => 'T_TYPE_UNION',
'content' => '|',
],
[
@@ -708,9 +707,8 @@ public function dataIdentifierTokenization()
'type' => 'T_STRING',
'content' => 'Unqualified',
],
- // TODO: change this to T_TYPE_UNION when #3032 is merged.
[
- 'type' => 'T_BITWISE_OR',
+ 'type' => 'T_TYPE_UNION',
'content' => '|',
],
[