Skip to content

Commit e4abe66

Browse files
committed
Changelog for #3581
2 parents 1fff686 + c803671 commit e4abe66

13 files changed

+678
-47
lines changed

package.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ http://pear.php.net/dtd/package-2.0.xsd">
3838
- Added support for the PHP 8.1 readonly token
3939
-- Tokenzing of the readonly keyword has been backfilled for PHP versions less than 8.1
4040
-- Thanks to Jaroslav Hanslík for the patch
41+
- Added support for PHP 8.1 intersection types
42+
-- Includes a new T_TYPE_INTERSECTION token to represent the ampersand character inside intersection types
43+
-- Thanks to Jaroslav Hanslík for the patch
4144
- File::getMethodParameters now supports the new PHP 8.1 readonly token
4245
-- When constructor property promotion is used, a new property_readonly array index is included in the return value
4346
--- This is a boolean value indicating if the property is readonly
@@ -199,6 +202,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
199202
<file baseinstalldir="" name="StableCommentWhitespaceTest.php" role="test" />
200203
<file baseinstalldir="" name="StableCommentWhitespaceWinTest.inc" role="test" />
201204
<file baseinstalldir="" name="StableCommentWhitespaceWinTest.php" role="test" />
205+
<file baseinstalldir="" name="TypeIntersectionTest.inc" role="test" />
206+
<file baseinstalldir="" name="TypeIntersectionTest.php" role="test" />
202207
<file baseinstalldir="" name="UndoNamespacedNameSingleTokenTest.inc" role="test" />
203208
<file baseinstalldir="" name="UndoNamespacedNameSingleTokenTest.php" role="test" />
204209
</dir>
@@ -2168,6 +2173,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
21682173
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceTest.inc" name="tests/Core/Tokenizer/StableCommentWhitespaceTest.inc" />
21692174
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceWinTest.php" name="tests/Core/Tokenizer/StableCommentWhitespaceWinTest.php" />
21702175
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceWinTest.inc" name="tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc" />
2176+
<install as="CodeSniffer/Core/Tokenizer/TypeIntersectionTest.php" name="tests/Core/Tokenizer/TypeIntersectionTest.php" />
2177+
<install as="CodeSniffer/Core/Tokenizer/TypeIntersectionTest.inc" name="tests/Core/Tokenizer/TypeIntersectionTest.inc" />
21712178
<install as="CodeSniffer/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php" name="tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php" />
21722179
<install as="CodeSniffer/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc" name="tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc" />
21732180
<install as="CodeSniffer/Standards/AllSniffs.php" name="tests/Standards/AllSniffs.php" />
@@ -2268,6 +2275,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
22682275
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceTest.inc" name="tests/Core/Tokenizer/StableCommentWhitespaceTest.inc" />
22692276
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceWinTest.php" name="tests/Core/Tokenizer/StableCommentWhitespaceWinTest.php" />
22702277
<install as="CodeSniffer/Core/Tokenizer/StableCommentWhitespaceWinTest.inc" name="tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc" />
2278+
<install as="CodeSniffer/Core/Tokenizer/TypeIntersectionTest.php" name="tests/Core/Tokenizer/TypeIntersectionTest.php" />
2279+
<install as="CodeSniffer/Core/Tokenizer/TypeIntersectionTest.inc" name="tests/Core/Tokenizer/TypeIntersectionTest.inc" />
22712280
<install as="CodeSniffer/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php" name="tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.php" />
22722281
<install as="CodeSniffer/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc" name="tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc" />
22732282
<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: 40 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
/**
@@ -2438,18 +2439,19 @@ protected function processAdditional()
24382439
if (isset($this->tokens[$x]) === true && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) {
24392440
$ignore = Util\Tokens::$emptyTokens;
24402441
$ignore += [
2441-
T_ARRAY => T_ARRAY,
2442-
T_CALLABLE => T_CALLABLE,
2443-
T_COLON => T_COLON,
2444-
T_NAMESPACE => T_NAMESPACE,
2445-
T_NS_SEPARATOR => T_NS_SEPARATOR,
2446-
T_NULL => T_NULL,
2447-
T_NULLABLE => T_NULLABLE,
2448-
T_PARENT => T_PARENT,
2449-
T_SELF => T_SELF,
2450-
T_STATIC => T_STATIC,
2451-
T_STRING => T_STRING,
2452-
T_TYPE_UNION => T_TYPE_UNION,
2442+
T_ARRAY => T_ARRAY,
2443+
T_CALLABLE => T_CALLABLE,
2444+
T_COLON => T_COLON,
2445+
T_NAMESPACE => T_NAMESPACE,
2446+
T_NS_SEPARATOR => T_NS_SEPARATOR,
2447+
T_NULL => T_NULL,
2448+
T_NULLABLE => T_NULLABLE,
2449+
T_PARENT => T_PARENT,
2450+
T_SELF => T_SELF,
2451+
T_STATIC => T_STATIC,
2452+
T_STRING => T_STRING,
2453+
T_TYPE_UNION => T_TYPE_UNION,
2454+
T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
24532455
];
24542456

24552457
$closer = $this->tokens[$x]['parenthesis_closer'];
@@ -2745,9 +2747,12 @@ protected function processAdditional()
27452747
}//end if
27462748

27472749
continue;
2748-
} else if ($this->tokens[$i]['code'] === T_BITWISE_OR) {
2750+
} else if ($this->tokens[$i]['code'] === T_BITWISE_OR
2751+
|| $this->tokens[$i]['code'] === T_BITWISE_AND
2752+
) {
27492753
/*
27502754
Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR.
2755+
Convert "&" to T_TYPE_INTERSECTION or leave as T_BITWISE_AND.
27512756
*/
27522757

27532758
$allowed = [
@@ -2812,12 +2817,12 @@ protected function processAdditional()
28122817
}//end for
28132818

28142819
if ($typeTokenCount === 0 || isset($suspectedType) === false) {
2815-
// Definitely not a union type, move on.
2820+
// Definitely not a union or intersection type, move on.
28162821
continue;
28172822
}
28182823

28192824
$typeTokenCount = 0;
2820-
$unionOperators = [$i];
2825+
$typeOperators = [$i];
28212826
$confirmed = false;
28222827

28232828
for ($x = ($i - 1); $x >= 0; $x--) {
@@ -2830,13 +2835,13 @@ protected function processAdditional()
28302835
continue;
28312836
}
28322837

2833-
// Union types can't use the nullable operator, but be tolerant to parse errors.
2838+
// Union and intersection types can't use the nullable operator, but be tolerant to parse errors.
28342839
if ($typeTokenCount > 0 && $this->tokens[$x]['code'] === T_NULLABLE) {
28352840
continue;
28362841
}
28372842

2838-
if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
2839-
$unionOperators[] = $x;
2843+
if ($this->tokens[$x]['code'] === T_BITWISE_OR || $this->tokens[$x]['code'] === T_BITWISE_AND) {
2844+
$typeOperators[] = $x;
28402845
continue;
28412846
}
28422847

@@ -2902,17 +2907,27 @@ protected function processAdditional()
29022907
}//end if
29032908

29042909
if ($confirmed === false) {
2905-
// Not a union type after all, move on.
2910+
// Not a union or intersection type after all, move on.
29062911
continue;
29072912
}
29082913

2909-
foreach ($unionOperators as $x) {
2910-
$this->tokens[$x]['code'] = T_TYPE_UNION;
2911-
$this->tokens[$x]['type'] = 'T_TYPE_UNION';
2914+
foreach ($typeOperators as $x) {
2915+
if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
2916+
$this->tokens[$x]['code'] = T_TYPE_UNION;
2917+
$this->tokens[$x]['type'] = 'T_TYPE_UNION';
29122918

2913-
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2914-
$line = $this->tokens[$x]['line'];
2915-
echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL;
2919+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2920+
$line = $this->tokens[$x]['line'];
2921+
echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL;
2922+
}
2923+
} else {
2924+
$this->tokens[$x]['code'] = T_TYPE_INTERSECTION;
2925+
$this->tokens[$x]['type'] = 'T_TYPE_INTERSECTION';
2926+
2927+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
2928+
$line = $this->tokens[$x]['line'];
2929+
echo "\t* token $x on line $line changed from T_BITWISE_AND to T_TYPE_INTERSECTION".PHP_EOL;
2930+
}
29162931
}
29172932
}
29182933

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
@@ -286,3 +286,19 @@ enum Direction implements ArrayAccess
286286
/* testEnumMethodParamNotProperty */
287287
public function offsetGet($val) { ... }
288288
}
289+
290+
$anon = class() {
291+
/* testPHP81IntersectionTypes */
292+
public Foo&Bar $intersectionType;
293+
294+
/* testPHP81MoreIntersectionTypes */
295+
public Foo&Bar&Baz $moreIntersectionTypes;
296+
297+
/* testPHP81IllegalIntersectionTypes */
298+
// Intentional fatal error - types which are not allowed for intersection type, but that's not the concern of the method.
299+
public int&string $illegalIntersectionType;
300+
301+
/* testPHP81NulltableIntersectionType */
302+
// Intentional fatal error - nullability is not allowed with intersection type, but that's not the concern of the method.
303+
public ?Foo&Bar $nullableIntersectionType;
304+
};

tests/Core/File/GetMemberPropertiesTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,46 @@ public function dataGetMemberProperties()
758758
'/* testEnumProperty */',
759759
[],
760760
],
761+
[
762+
'/* testPHP81IntersectionTypes */',
763+
[
764+
'scope' => 'public',
765+
'scope_specified' => true,
766+
'is_static' => false,
767+
'type' => 'Foo&Bar',
768+
'nullable_type' => false,
769+
],
770+
],
771+
[
772+
'/* testPHP81MoreIntersectionTypes */',
773+
[
774+
'scope' => 'public',
775+
'scope_specified' => true,
776+
'is_static' => false,
777+
'type' => 'Foo&Bar&Baz',
778+
'nullable_type' => false,
779+
],
780+
],
781+
[
782+
'/* testPHP81IllegalIntersectionTypes */',
783+
[
784+
'scope' => 'public',
785+
'scope_specified' => true,
786+
'is_static' => false,
787+
'type' => 'int&string',
788+
'nullable_type' => false,
789+
],
790+
],
791+
[
792+
'/* testPHP81NulltableIntersectionType */',
793+
[
794+
'scope' => 'public',
795+
'scope_specified' => true,
796+
'is_static' => false,
797+
'type' => '?Foo&Bar',
798+
'nullable_type' => true,
799+
],
800+
],
761801
];
762802

763803
}//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)