diff --git a/package.xml b/package.xml
index 4a1151a7df..41fe14e2b4 100644
--- a/package.xml
+++ b/package.xml
@@ -199,6 +199,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
@@ -2168,6 +2170,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
@@ -2268,6 +2272,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
diff --git a/src/Files/File.php b/src/Files/File.php
index b0e67cec10..a15af762fc 100644
--- a/src/Files/File.php
+++ b/src/Files/File.php
@@ -1469,6 +1469,7 @@ public function getMethodParameters($stackPtr)
case T_NAMESPACE:
case T_NS_SEPARATOR:
case T_TYPE_UNION:
+ case T_TYPE_INTERSECTION:
case T_FALSE:
case T_NULL:
// Part of a type hint or default value.
@@ -1685,16 +1686,17 @@ public function getMethodProperties($stackPtr)
}
$valid = [
- 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_NAMESPACE => T_NAMESPACE,
- T_NS_SEPARATOR => T_NS_SEPARATOR,
- T_TYPE_UNION => T_TYPE_UNION,
+ 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_NAMESPACE => T_NAMESPACE,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_TYPE_UNION => T_TYPE_UNION,
+ T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
];
for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
@@ -1886,15 +1888,16 @@ public function getMemberProperties($stackPtr)
if ($i < $stackPtr) {
// We've found a type.
$valid = [
- T_STRING => T_STRING,
- 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,
+ T_STRING => T_STRING,
+ 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,
+ T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
];
for ($i; $i < $stackPtr; $i++) {
diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php
index 62894393f7..96331f11db 100644
--- a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php
@@ -103,7 +103,9 @@ public function process(File $phpcsFile, $stackPtr)
$error = 'PHP property type declarations must be lowercase; expected "%s" but found "%s"';
$errorCode = 'PropertyTypeFound';
- if (strpos($type, '|') !== false) {
+ if ($props['type_token'] === T_TYPE_INTERSECTION) {
+ // Intersection types don't support simple types.
+ } else if (strpos($type, '|') !== false) {
$this->processUnionType(
$phpcsFile,
$props['type_token'],
@@ -132,7 +134,9 @@ public function process(File $phpcsFile, $stackPtr)
$error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"';
$errorCode = 'ReturnTypeFound';
- if (strpos($returnType, '|') !== false) {
+ if ($props['return_type_token'] === T_TYPE_INTERSECTION) {
+ // Intersection types don't support simple types.
+ } else if (strpos($returnType, '|') !== false) {
$this->processUnionType(
$phpcsFile,
$props['return_type_token'],
@@ -162,7 +166,9 @@ public function process(File $phpcsFile, $stackPtr)
$error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"';
$errorCode = 'ParamTypeFound';
- if (strpos($typeHint, '|') !== false) {
+ if ($param['type_hint_token'] === T_TYPE_INTERSECTION) {
+ // Intersection types don't support simple types.
+ } else if (strpos($typeHint, '|') !== false) {
$this->processUnionType(
$phpcsFile,
$param['type_hint_token'],
diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php
index d61d72d1e7..5233a59202 100644
--- a/src/Tokenizers/PHP.php
+++ b/src/Tokenizers/PHP.php
@@ -462,6 +462,7 @@ class PHP extends Tokenizer
T_OPEN_SHORT_ARRAY => 1,
T_CLOSE_SHORT_ARRAY => 1,
T_TYPE_UNION => 1,
+ T_TYPE_INTERSECTION => 1,
];
/**
@@ -2406,18 +2407,19 @@ protected function processAdditional()
if (isset($this->tokens[$x]) === true && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) {
$ignore = Util\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_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_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_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,
];
$closer = $this->tokens[$x]['parenthesis_closer'];
@@ -2713,9 +2715,12 @@ protected function processAdditional()
}//end if
continue;
- } else if ($this->tokens[$i]['code'] === T_BITWISE_OR) {
+ } else if ($this->tokens[$i]['code'] === T_BITWISE_OR
+ || $this->tokens[$i]['code'] === T_BITWISE_AND
+ ) {
/*
Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR.
+ Convert "&" to T_TYPE_INTERSECTION or leave as T_BITWISE_AND.
*/
$allowed = [
@@ -2780,12 +2785,12 @@ protected function processAdditional()
}//end for
if ($typeTokenCount === 0 || isset($suspectedType) === false) {
- // Definitely not a union type, move on.
+ // Definitely not a union or intersection type, move on.
continue;
}
$typeTokenCount = 0;
- $unionOperators = [$i];
+ $typeOperators = [$i];
$confirmed = false;
for ($x = ($i - 1); $x >= 0; $x--) {
@@ -2798,13 +2803,13 @@ protected function processAdditional()
continue;
}
- // Union types can't use the nullable operator, but be tolerant to parse errors.
+ // 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) {
continue;
}
- if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
- $unionOperators[] = $x;
+ if ($this->tokens[$x]['code'] === T_BITWISE_OR || $this->tokens[$x]['code'] === T_BITWISE_AND) {
+ $typeOperators[] = $x;
continue;
}
@@ -2870,17 +2875,27 @@ protected function processAdditional()
}//end if
if ($confirmed === false) {
- // Not a union type after all, move on.
+ // Not a union or intersection type after all, move on.
continue;
}
- foreach ($unionOperators as $x) {
- $this->tokens[$x]['code'] = T_TYPE_UNION;
- $this->tokens[$x]['type'] = 'T_TYPE_UNION';
+ foreach ($typeOperators as $x) {
+ if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
+ $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;
+ 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;
+ }
+ } else {
+ $this->tokens[$x]['code'] = T_TYPE_INTERSECTION;
+ $this->tokens[$x]['type'] = 'T_TYPE_INTERSECTION';
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $line = $this->tokens[$x]['line'];
+ echo "\t* token $x on line $line changed from T_BITWISE_AND to T_TYPE_INTERSECTION".PHP_EOL;
+ }
}
}
@@ -2938,6 +2953,7 @@ protected function processAdditional()
T_NAME_RELATIVE => T_NAME_RELATIVE,
T_NAME_QUALIFIED => T_NAME_QUALIFIED,
T_TYPE_UNION => T_TYPE_UNION,
+ T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
T_BITWISE_OR => T_BITWISE_OR,
T_BITWISE_AND => T_BITWISE_AND,
T_ARRAY => T_ARRAY,
diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php
index febd9c5732..53e5ef4b98 100644
--- a/src/Util/Tokens.php
+++ b/src/Util/Tokens.php
@@ -81,6 +81,7 @@
define('T_MATCH_DEFAULT', 'PHPCS_T_MATCH_DEFAULT');
define('T_ATTRIBUTE_END', 'PHPCS_T_ATTRIBUTE_END');
define('T_ENUM_CASE', 'PHPCS_T_ENUM_CASE');
+define('T_TYPE_INTERSECTION', 'PHPCS_T_TYPE_INTERSECTION');
// 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 4533c5ffaf..044caec185 100644
--- a/tests/Core/File/GetMemberPropertiesTest.inc
+++ b/tests/Core/File/GetMemberPropertiesTest.inc
@@ -280,3 +280,19 @@ enum Direction implements ArrayAccess
/* testEnumMethodParamNotProperty */
public function offsetGet($val) { ... }
}
+
+$anon = class() {
+ /* testPHP81IntersectionTypes */
+ public Foo&Bar $intersectionType;
+
+ /* testPHP81MoreIntersectionTypes */
+ public Foo&Bar&Baz $moreIntersectionTypes;
+
+ /* testPHP81IllegalIntersectionTypes */
+ // Intentional fatal error - types which are not allowed for intersection type, but that's not the concern of the method.
+ public int&string $illegalIntersectionType;
+
+ /* testPHP81NulltableIntersectionType */
+ // Intentional fatal error - nullability is not allowed with intersection type, but that's not the concern of the method.
+ public ?Foo&Bar $nullableIntersectionType;
+};
diff --git a/tests/Core/File/GetMemberPropertiesTest.php b/tests/Core/File/GetMemberPropertiesTest.php
index cc709e69c7..ae16a778fe 100644
--- a/tests/Core/File/GetMemberPropertiesTest.php
+++ b/tests/Core/File/GetMemberPropertiesTest.php
@@ -736,6 +736,46 @@ public function dataGetMemberProperties()
'/* testEnumProperty */',
[],
],
+ [
+ '/* testPHP81IntersectionTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'Foo&Bar',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81MoreIntersectionTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'Foo&Bar&Baz',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81IllegalIntersectionTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'int&string',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81NulltableIntersectionType */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => '?Foo&Bar',
+ 'nullable_type' => true,
+ ],
+ ],
];
}//end dataGetMemberProperties()
diff --git a/tests/Core/File/GetMethodParametersTest.inc b/tests/Core/File/GetMethodParametersTest.inc
index e7565e4cad..dc46549140 100644
--- a/tests/Core/File/GetMethodParametersTest.inc
+++ b/tests/Core/File/GetMethodParametersTest.inc
@@ -145,3 +145,20 @@ class ParametersWithAttributes(
&...$otherParam,
) {}
}
+
+/* testPHP8IntersectionTypes */
+function intersectionTypes(Foo&Bar $obj1, Boo&Bar $obj2) {}
+
+/* testPHP81IntersectionTypesWithSpreadOperatorAndReference */
+function globalFunctionWithSpreadAndReference(Boo&Bar &$paramA, Foo&Bar ...$paramB) {}
+
+/* testPHP81MoreIntersectionTypes */
+function moreIntersectionTypes(MyClassA&\Package\MyClassB&\Package\MyClassC $var) {}
+
+/* testPHP81IllegalIntersectionTypes */
+// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method.
+$closure = function (string&int $numeric_string) {};
+
+/* testPHP81NullableIntersectionTypes */
+// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method.
+$closure = function (?Foo&Bar $object) {};
diff --git a/tests/Core/File/GetMethodParametersTest.php b/tests/Core/File/GetMethodParametersTest.php
index e07bd869d3..f70d64f2d1 100644
--- a/tests/Core/File/GetMethodParametersTest.php
+++ b/tests/Core/File/GetMethodParametersTest.php
@@ -991,6 +991,139 @@ public function testParameterAttributesInFunctionDeclaration()
}//end testParameterAttributesInFunctionDeclaration()
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration.
+ *
+ * @return void
+ */
+ public function testPHP8IntersectionTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$obj1',
+ 'content' => 'Foo&Bar $obj1',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'Foo&Bar',
+ 'nullable_type' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$obj2',
+ 'content' => 'Boo&Bar $obj2',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'Boo&Bar',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8IntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8 intersection type declaration when the variable has either a spread operator or a reference.
+ *
+ * @return void
+ */
+ public function testPHP81IntersectionTypesWithSpreadOperatorAndReference()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$paramA',
+ 'content' => 'Boo&Bar &$paramA',
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => false,
+ 'type_hint' => 'Boo&Bar',
+ 'nullable_type' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$paramB',
+ 'content' => 'Foo&Bar ...$paramB',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => true,
+ 'type_hint' => 'Foo&Bar',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81IntersectionTypesWithSpreadOperatorAndReference()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with more types.
+ *
+ * @return void
+ */
+ public function testPHP81MoreIntersectionTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'MyClassA&\Package\MyClassB&\Package\MyClassC $var',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'MyClassA&\Package\MyClassB&\Package\MyClassC',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81MoreIntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with illegal simple types.
+ *
+ * @return void
+ */
+ public function testPHP81IllegalIntersectionTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$numeric_string',
+ 'content' => 'string&int $numeric_string',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'string&int',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81IllegalIntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP81NullableIntersectionTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$object',
+ 'content' => '?Foo&Bar $object',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?Foo&Bar',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81NullableIntersectionTypes()
+
+
/**
* Test helper.
*
diff --git a/tests/Core/File/GetMethodPropertiesTest.inc b/tests/Core/File/GetMethodPropertiesTest.inc
index d890aad92b..0c592369a5 100644
--- a/tests/Core/File/GetMethodPropertiesTest.inc
+++ b/tests/Core/File/GetMethodPropertiesTest.inc
@@ -133,3 +133,20 @@ function never(): never {}
/* testPHP81NullableNeverType */
// Intentional fatal error - nullability is not allowed with never, but that's not the concern of the method.
function nullableNever(): ?never {}
+
+/* testPHP8IntersectionTypes */
+function intersectionTypes(): Foo&Bar {}
+
+/* testPHP81MoreIntersectionTypes */
+function moreIntersectionTypes(): MyClassA&\Package\MyClassB&\Package\MyClassC {}
+
+/* testPHP81IntersectionArrowFunction */
+$fn = fn($var): MyClassA&\Package\MyClassB => $var;
+
+/* testPHP81IllegalIntersectionTypes */
+// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method.
+$closure = function (): string&int {};
+
+/* testPHP81NullableIntersectionTypes */
+// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method.
+$closure = function (): ?Foo&Bar {};
diff --git a/tests/Core/File/GetMethodPropertiesTest.php b/tests/Core/File/GetMethodPropertiesTest.php
index 9872cf3987..2f552bfecd 100644
--- a/tests/Core/File/GetMethodPropertiesTest.php
+++ b/tests/Core/File/GetMethodPropertiesTest.php
@@ -774,6 +774,122 @@ public function testPHP81NullableNeverType()
}//end testPHP81NullableNeverType()
+ /**
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration.
+ *
+ * @return void
+ */
+ public function testPHP8IntersectionTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'Foo&Bar',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8IntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with more types.
+ *
+ * @return void
+ */
+ public function testPHP81MoreIntersectionTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'MyClassA&\Package\MyClassB&\Package\MyClassC',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81MoreIntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration in arrow function.
+ *
+ * @return void
+ */
+ public function testPHP81IntersectionArrowFunction()
+ {
+ $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 testPHP81IntersectionArrowFunction()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with illegal simple types.
+ *
+ * @return void
+ */
+ public function testPHP81IllegalIntersectionTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'string&int',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81IllegalIntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP81NullableIntersectionTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => '?Foo&Bar',
+ 'nullable_return_type' => true,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81NullableIntersectionTypes()
+
+
/**
* Test helper.
*
diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.inc b/tests/Core/Tokenizer/TypeIntersectionTest.inc
new file mode 100644
index 0000000000..abf9b85ba8
--- /dev/null
+++ b/tests/Core/Tokenizer/TypeIntersectionTest.inc
@@ -0,0 +1,120 @@
+ $param & $int;
+
+/* testTypeIntersectionArrowReturnType */
+$arrowWithReturnType = fn ($param) : Foo&Bar => $param * 10;
+
+/* testBitwiseAndInArrayKey */
+$array = array(
+ A & B => /* testBitwiseAndInArrayValue */ B & C
+);
+
+/* testBitwiseAndInShortArrayKey */
+$array = [
+ A & B => /* testBitwiseAndInShortArrayValue */ B & C
+];
+
+/* testBitwiseAndNonArrowFnFunctionCall */
+$obj->fn($something & $else);
+
+/* testBitwiseAnd6 */
+function &fn(/* testTypeIntersectionNonArrowFunctionDeclaration */ Foo&Bar $something) {}
+
+/* testTypeIntersectionWithInvalidTypes */
+function (int&string $var) {};
+
+/* testLiveCoding */
+// Intentional parse error. This has to be the last test in the file.
+return function( Foo&
diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.php b/tests/Core/Tokenizer/TypeIntersectionTest.php
new file mode 100644
index 0000000000..2170021933
--- /dev/null
+++ b/tests/Core/Tokenizer/TypeIntersectionTest.php
@@ -0,0 +1,138 @@
+
+ * @author Jaroslav HanslĂk
+ * @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 TypeIntersectionTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that non-intersection type bitwise and tokens are still tokenized as bitwise and.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataBitwiseAnd
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testBitwiseAnd($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_BITWISE_AND, T_TYPE_INTERSECTION]);
+ $this->assertSame(T_BITWISE_AND, $tokens[$opener]['code']);
+ $this->assertSame('T_BITWISE_AND', $tokens[$opener]['type']);
+
+ }//end testBitwiseAnd()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testBitwiseAnd()
+ *
+ * @return array
+ */
+ public function dataBitwiseAnd()
+ {
+ return [
+ ['/* testBitwiseAnd1 */'],
+ ['/* testBitwiseAnd2 */'],
+ ['/* testBitwiseAndPropertyDefaultValue */'],
+ ['/* testBitwiseAndParamDefaultValue */'],
+ ['/* testBitwiseAnd3 */'],
+ ['/* testBitwiseAnd4 */'],
+ ['/* testBitwiseAnd5 */'],
+ ['/* testBitwiseAndClosureParamDefault */'],
+ ['/* testBitwiseAndArrowParamDefault */'],
+ ['/* testBitwiseAndArrowExpression */'],
+ ['/* testBitwiseAndInArrayKey */'],
+ ['/* testBitwiseAndInArrayValue */'],
+ ['/* testBitwiseAndInShortArrayKey */'],
+ ['/* testBitwiseAndInShortArrayValue */'],
+ ['/* testBitwiseAndNonArrowFnFunctionCall */'],
+ ['/* testBitwiseAnd6 */'],
+ ['/* testLiveCoding */'],
+ ];
+
+ }//end dataBitwiseAnd()
+
+
+ /**
+ * Test that bitwise and tokens when used as part of a intersection type are tokenized as `T_TYPE_INTERSECTION`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataTypeIntersection
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testTypeIntersection($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_BITWISE_AND, T_TYPE_INTERSECTION]);
+ $this->assertSame(T_TYPE_INTERSECTION, $tokens[$opener]['code']);
+ $this->assertSame('T_TYPE_INTERSECTION', $tokens[$opener]['type']);
+
+ }//end testTypeIntersection()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testTypeIntersection()
+ *
+ * @return array
+ */
+ public function dataTypeIntersection()
+ {
+ return [
+ ['/* testTypeIntersectionPropertySimple */'],
+ ['/* testTypeIntersectionPropertyReverseModifierOrder */'],
+ ['/* testTypeIntersectionPropertyMulti1 */'],
+ ['/* testTypeIntersectionPropertyMulti2 */'],
+ ['/* testTypeIntersectionPropertyMulti3 */'],
+ ['/* testTypeIntersectionPropertyNamespaceRelative */'],
+ ['/* testTypeIntersectionPropertyPartiallyQualified */'],
+ ['/* testTypeIntersectionPropertyFullyQualified */'],
+ ['/* testTypeIntersectionPropertyWithReadOnlyKeyword */'],
+ ['/* testTypeIntersectionParam1 */'],
+ ['/* testTypeIntersectionParam2 */'],
+ ['/* testTypeIntersectionParam3 */'],
+ ['/* testTypeIntersectionParamNamespaceRelative */'],
+ ['/* testTypeIntersectionParamPartiallyQualified */'],
+ ['/* testTypeIntersectionParamFullyQualified */'],
+ ['/* testTypeIntersectionReturnType */'],
+ ['/* testTypeIntersectionConstructorPropertyPromotion */'],
+ ['/* testTypeIntersectionAbstractMethodReturnType1 */'],
+ ['/* testTypeIntersectionAbstractMethodReturnType2 */'],
+ ['/* testTypeIntersectionReturnTypeNamespaceRelative */'],
+ ['/* testTypeIntersectionReturnPartiallyQualified */'],
+ ['/* testTypeIntersectionReturnFullyQualified */'],
+ ['/* testTypeIntersectionClosureParamIllegalNullable */'],
+ ['/* testTypeIntersectionWithReference */'],
+ ['/* testTypeIntersectionWithSpreadOperator */'],
+ ['/* testTypeIntersectionClosureReturn */'],
+ ['/* testTypeIntersectionArrowParam */'],
+ ['/* testTypeIntersectionArrowReturnType */'],
+ ['/* testTypeIntersectionNonArrowFunctionDeclaration */'],
+ ['/* testTypeIntersectionWithInvalidTypes */'],
+ ];
+
+ }//end dataTypeIntersection()
+
+
+}//end class