Skip to content

Commit c803671

Browse files
committed
PHP 8.1: Support of intersection types
1 parent d009ba6 commit c803671

13 files changed

+676
-47
lines changed

package.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
199199
<file baseinstalldir="" name="StableCommentWhitespaceTest.php" role="test" />
200200
<file baseinstalldir="" name="StableCommentWhitespaceWinTest.inc" role="test" />
201201
<file baseinstalldir="" name="StableCommentWhitespaceWinTest.php" role="test" />
202+
<file baseinstalldir="" name="TypeIntersectionTest.inc" role="test" />
203+
<file baseinstalldir="" name="TypeIntersectionTest.php" role="test" />
202204
<file baseinstalldir="" name="UndoNamespacedNameSingleTokenTest.inc" role="test" />
203205
<file baseinstalldir="" name="UndoNamespacedNameSingleTokenTest.php" role="test" />
204206
</dir>
@@ -2168,6 +2170,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
21682170
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceTest.inc" name="tests/Core/Tokenizer/StableCommentWhitespaceTest.inc" />
21692171
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceWinTest.php" name="tests/Core/Tokenizer/StableCommentWhitespaceWinTest.php" />
21702172
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceWinTest.inc" name="tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc" />
2173+
<install as="CodeSniffer/Core/Tokenizer/TypeIntersectionTest.php" name="tests/Core/Tokenizer/TypeIntersectionTest.php" />
2174+
<install as="CodeSniffer/Core/Tokenizer/TypeIntersectionTest.inc" name="tests/Core/Tokenizer/TypeIntersectionTest.inc" />
21712175
<install as="CodeSniffer/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php" name="tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php" />
21722176
<install as="CodeSniffer/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc" name="tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc" />
21732177
<install as="CodeSniffer/Standards/AllSniffs.php" name="tests/Standards/AllSniffs.php" />
@@ -2268,6 +2272,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
22682272
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceTest.inc" name="tests/Core/Tokenizer/StableCommentWhitespaceTest.inc" />
22692273
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceWinTest.php" name="tests/Core/Tokenizer/StableCommentWhitespaceWinTest.php" />
22702274
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceWinTest.inc" name="tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc" />
2275+
<install as="CodeSniffer/Core/Tokenizer/TypeIntersectionTest.php" name="tests/Core/Tokenizer/TypeIntersectionTest.php" />
2276+
<install as="CodeSniffer/Core/Tokenizer/TypeIntersectionTest.inc" name="tests/Core/Tokenizer/TypeIntersectionTest.inc" />
22712277
<install as="CodeSniffer/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php" name="tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php" />
22722278
<install as="CodeSniffer/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc" name="tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc" />
22732279
<install as="CodeSniffer/Standards/AllSniffs.php" name="tests/Standards/AllSniffs.php" />

src/Files/File.php

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,7 @@ public function getMethodParameters($stackPtr)
14691469
case T_NAMESPACE:
14701470
case T_NS_SEPARATOR:
14711471
case T_TYPE_UNION:
1472+
case T_TYPE_INTERSECTION:
14721473
case T_FALSE:
14731474
case T_NULL:
14741475
// Part of a type hint or default value.
@@ -1685,16 +1686,17 @@ public function getMethodProperties($stackPtr)
16851686
}
16861687

16871688
$valid = [
1688-
T_STRING => T_STRING,
1689-
T_CALLABLE => T_CALLABLE,
1690-
T_SELF => T_SELF,
1691-
T_PARENT => T_PARENT,
1692-
T_STATIC => T_STATIC,
1693-
T_FALSE => T_FALSE,
1694-
T_NULL => T_NULL,
1695-
T_NAMESPACE => T_NAMESPACE,
1696-
T_NS_SEPARATOR => T_NS_SEPARATOR,
1697-
T_TYPE_UNION => T_TYPE_UNION,
1689+
T_STRING => T_STRING,
1690+
T_CALLABLE => T_CALLABLE,
1691+
T_SELF => T_SELF,
1692+
T_PARENT => T_PARENT,
1693+
T_STATIC => T_STATIC,
1694+
T_FALSE => T_FALSE,
1695+
T_NULL => T_NULL,
1696+
T_NAMESPACE => T_NAMESPACE,
1697+
T_NS_SEPARATOR => T_NS_SEPARATOR,
1698+
T_TYPE_UNION => T_TYPE_UNION,
1699+
T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
16981700
];
16991701

17001702
for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
@@ -1886,15 +1888,16 @@ public function getMemberProperties($stackPtr)
18861888
if ($i < $stackPtr) {
18871889
// We've found a type.
18881890
$valid = [
1889-
T_STRING => T_STRING,
1890-
T_CALLABLE => T_CALLABLE,
1891-
T_SELF => T_SELF,
1892-
T_PARENT => T_PARENT,
1893-
T_FALSE => T_FALSE,
1894-
T_NULL => T_NULL,
1895-
T_NAMESPACE => T_NAMESPACE,
1896-
T_NS_SEPARATOR => T_NS_SEPARATOR,
1897-
T_TYPE_UNION => T_TYPE_UNION,
1891+
T_STRING => T_STRING,
1892+
T_CALLABLE => T_CALLABLE,
1893+
T_SELF => T_SELF,
1894+
T_PARENT => T_PARENT,
1895+
T_FALSE => T_FALSE,
1896+
T_NULL => T_NULL,
1897+
T_NAMESPACE => T_NAMESPACE,
1898+
T_NS_SEPARATOR => T_NS_SEPARATOR,
1899+
T_TYPE_UNION => T_TYPE_UNION,
1900+
T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
18981901
];
18991902

19001903
for ($i; $i < $stackPtr; $i++) {

src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ public function process(File $phpcsFile, $stackPtr)
103103
$error = 'PHP property type declarations must be lowercase; expected "%s" but found "%s"';
104104
$errorCode = 'PropertyTypeFound';
105105

106-
if (strpos($type, '|') !== false) {
106+
if ($props['type_token'] === T_TYPE_INTERSECTION) {
107+
// Intersection types don't support simple types.
108+
} else if (strpos($type, '|') !== false) {
107109
$this->processUnionType(
108110
$phpcsFile,
109111
$props['type_token'],
@@ -132,7 +134,9 @@ public function process(File $phpcsFile, $stackPtr)
132134
$error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"';
133135
$errorCode = 'ReturnTypeFound';
134136

135-
if (strpos($returnType, '|') !== false) {
137+
if ($props['return_type_token'] === T_TYPE_INTERSECTION) {
138+
// Intersection types don't support simple types.
139+
} else if (strpos($returnType, '|') !== false) {
136140
$this->processUnionType(
137141
$phpcsFile,
138142
$props['return_type_token'],
@@ -162,7 +166,9 @@ public function process(File $phpcsFile, $stackPtr)
162166
$error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"';
163167
$errorCode = 'ParamTypeFound';
164168

165-
if (strpos($typeHint, '|') !== false) {
169+
if ($param['type_hint_token'] === T_TYPE_INTERSECTION) {
170+
// Intersection types don't support simple types.
171+
} else if (strpos($typeHint, '|') !== false) {
166172
$this->processUnionType(
167173
$phpcsFile,
168174
$param['type_hint_token'],

src/Tokenizers/PHP.php

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ class PHP extends Tokenizer
462462
T_OPEN_SHORT_ARRAY => 1,
463463
T_CLOSE_SHORT_ARRAY => 1,
464464
T_TYPE_UNION => 1,
465+
T_TYPE_INTERSECTION => 1,
465466
];
466467

467468
/**
@@ -2406,18 +2407,19 @@ protected function processAdditional()
24062407
if (isset($this->tokens[$x]) === true && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) {
24072408
$ignore = Util\Tokens::$emptyTokens;
24082409
$ignore += [
2409-
T_ARRAY => T_ARRAY,
2410-
T_CALLABLE => T_CALLABLE,
2411-
T_COLON => T_COLON,
2412-
T_NAMESPACE => T_NAMESPACE,
2413-
T_NS_SEPARATOR => T_NS_SEPARATOR,
2414-
T_NULL => T_NULL,
2415-
T_NULLABLE => T_NULLABLE,
2416-
T_PARENT => T_PARENT,
2417-
T_SELF => T_SELF,
2418-
T_STATIC => T_STATIC,
2419-
T_STRING => T_STRING,
2420-
T_TYPE_UNION => T_TYPE_UNION,
2410+
T_ARRAY => T_ARRAY,
2411+
T_CALLABLE => T_CALLABLE,
2412+
T_COLON => T_COLON,
2413+
T_NAMESPACE => T_NAMESPACE,
2414+
T_NS_SEPARATOR => T_NS_SEPARATOR,
2415+
T_NULL => T_NULL,
2416+
T_NULLABLE => T_NULLABLE,
2417+
T_PARENT => T_PARENT,
2418+
T_SELF => T_SELF,
2419+
T_STATIC => T_STATIC,
2420+
T_STRING => T_STRING,
2421+
T_TYPE_UNION => T_TYPE_UNION,
2422+
T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
24212423
];
24222424

24232425
$closer = $this->tokens[$x]['parenthesis_closer'];
@@ -2713,9 +2715,12 @@ protected function processAdditional()
27132715
}//end if
27142716

27152717
continue;
2716-
} else if ($this->tokens[$i]['code'] === T_BITWISE_OR) {
2718+
} else if ($this->tokens[$i]['code'] === T_BITWISE_OR
2719+
|| $this->tokens[$i]['code'] === T_BITWISE_AND
2720+
) {
27172721
/*
27182722
Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR.
2723+
Convert "&" to T_TYPE_INTERSECTION or leave as T_BITWISE_AND.
27192724
*/
27202725

27212726
$allowed = [
@@ -2780,12 +2785,12 @@ protected function processAdditional()
27802785
}//end for
27812786

27822787
if ($typeTokenCount === 0 || isset($suspectedType) === false) {
2783-
// Definitely not a union type, move on.
2788+
// Definitely not a union or intersection type, move on.
27842789
continue;
27852790
}
27862791

27872792
$typeTokenCount = 0;
2788-
$unionOperators = [$i];
2793+
$typeOperators = [$i];
27892794
$confirmed = false;
27902795

27912796
for ($x = ($i - 1); $x >= 0; $x--) {
@@ -2798,13 +2803,13 @@ protected function processAdditional()
27982803
continue;
27992804
}
28002805

2801-
// Union types can't use the nullable operator, but be tolerant to parse errors.
2806+
// Union and intersection types can't use the nullable operator, but be tolerant to parse errors.
28022807
if ($typeTokenCount > 0 && $this->tokens[$x]['code'] === T_NULLABLE) {
28032808
continue;
28042809
}
28052810

2806-
if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
2807-
$unionOperators[] = $x;
2811+
if ($this->tokens[$x]['code'] === T_BITWISE_OR || $this->tokens[$x]['code'] === T_BITWISE_AND) {
2812+
$typeOperators[] = $x;
28082813
continue;
28092814
}
28102815

@@ -2870,17 +2875,27 @@ protected function processAdditional()
28702875
}//end if
28712876

28722877
if ($confirmed === false) {
2873-
// Not a union type after all, move on.
2878+
// Not a union or intersection type after all, move on.
28742879
continue;
28752880
}
28762881

2877-
foreach ($unionOperators as $x) {
2878-
$this->tokens[$x]['code'] = T_TYPE_UNION;
2879-
$this->tokens[$x]['type'] = 'T_TYPE_UNION';
2882+
foreach ($typeOperators as $x) {
2883+
if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
2884+
$this->tokens[$x]['code'] = T_TYPE_UNION;
2885+
$this->tokens[$x]['type'] = 'T_TYPE_UNION';
28802886

2881-
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2882-
$line = $this->tokens[$x]['line'];
2883-
echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL;
2887+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2888+
$line = $this->tokens[$x]['line'];
2889+
echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL;
2890+
}
2891+
} else {
2892+
$this->tokens[$x]['code'] = T_TYPE_INTERSECTION;
2893+
$this->tokens[$x]['type'] = 'T_TYPE_INTERSECTION';
2894+
2895+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2896+
$line = $this->tokens[$x]['line'];
2897+
echo "\t* token $x on line $line changed from T_BITWISE_AND to T_TYPE_INTERSECTION".PHP_EOL;
2898+
}
28842899
}
28852900
}
28862901

@@ -2938,6 +2953,7 @@ protected function processAdditional()
29382953
T_NAME_RELATIVE => T_NAME_RELATIVE,
29392954
T_NAME_QUALIFIED => T_NAME_QUALIFIED,
29402955
T_TYPE_UNION => T_TYPE_UNION,
2956+
T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
29412957
T_BITWISE_OR => T_BITWISE_OR,
29422958
T_BITWISE_AND => T_BITWISE_AND,
29432959
T_ARRAY => T_ARRAY,

src/Util/Tokens.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
define('T_MATCH_DEFAULT', 'PHPCS_T_MATCH_DEFAULT');
8282
define('T_ATTRIBUTE_END', 'PHPCS_T_ATTRIBUTE_END');
8383
define('T_ENUM_CASE', 'PHPCS_T_ENUM_CASE');
84+
define('T_TYPE_INTERSECTION', 'PHPCS_T_TYPE_INTERSECTION');
8485

8586
// Some PHP 5.5 tokens, replicated for lower versions.
8687
if (defined('T_FINALLY') === false) {

tests/Core/File/GetMemberPropertiesTest.inc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,19 @@ enum Direction implements ArrayAccess
280280
/* testEnumMethodParamNotProperty */
281281
public function offsetGet($val) { ... }
282282
}
283+
284+
$anon = class() {
285+
/* testPHP81IntersectionTypes */
286+
public Foo&Bar $intersectionType;
287+
288+
/* testPHP81MoreIntersectionTypes */
289+
public Foo&Bar&Baz $moreIntersectionTypes;
290+
291+
/* testPHP81IllegalIntersectionTypes */
292+
// Intentional fatal error - types which are not allowed for intersection type, but that's not the concern of the method.
293+
public int&string $illegalIntersectionType;
294+
295+
/* testPHP81NulltableIntersectionType */
296+
// Intentional fatal error - nullability is not allowed with intersection type, but that's not the concern of the method.
297+
public ?Foo&Bar $nullableIntersectionType;
298+
};

tests/Core/File/GetMemberPropertiesTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,46 @@ public function dataGetMemberProperties()
736736
'/* testEnumProperty */',
737737
[],
738738
],
739+
[
740+
'/* testPHP81IntersectionTypes */',
741+
[
742+
'scope' => 'public',
743+
'scope_specified' => true,
744+
'is_static' => false,
745+
'type' => 'Foo&Bar',
746+
'nullable_type' => false,
747+
],
748+
],
749+
[
750+
'/* testPHP81MoreIntersectionTypes */',
751+
[
752+
'scope' => 'public',
753+
'scope_specified' => true,
754+
'is_static' => false,
755+
'type' => 'Foo&Bar&Baz',
756+
'nullable_type' => false,
757+
],
758+
],
759+
[
760+
'/* testPHP81IllegalIntersectionTypes */',
761+
[
762+
'scope' => 'public',
763+
'scope_specified' => true,
764+
'is_static' => false,
765+
'type' => 'int&string',
766+
'nullable_type' => false,
767+
],
768+
],
769+
[
770+
'/* testPHP81NulltableIntersectionType */',
771+
[
772+
'scope' => 'public',
773+
'scope_specified' => true,
774+
'is_static' => false,
775+
'type' => '?Foo&Bar',
776+
'nullable_type' => true,
777+
],
778+
],
739779
];
740780

741781
}//end dataGetMemberProperties()

tests/Core/File/GetMethodParametersTest.inc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,20 @@ class ParametersWithAttributes(
145145
&...$otherParam,
146146
) {}
147147
}
148+
149+
/* testPHP8IntersectionTypes */
150+
function intersectionTypes(Foo&Bar $obj1, Boo&Bar $obj2) {}
151+
152+
/* testPHP81IntersectionTypesWithSpreadOperatorAndReference */
153+
function globalFunctionWithSpreadAndReference(Boo&Bar &$paramA, Foo&Bar ...$paramB) {}
154+
155+
/* testPHP81MoreIntersectionTypes */
156+
function moreIntersectionTypes(MyClassA&\Package\MyClassB&\Package\MyClassC $var) {}
157+
158+
/* testPHP81IllegalIntersectionTypes */
159+
// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method.
160+
$closure = function (string&int $numeric_string) {};
161+
162+
/* testPHP81NullableIntersectionTypes */
163+
// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method.
164+
$closure = function (?Foo&Bar $object) {};

0 commit comments

Comments
 (0)