Skip to content

Commit a60544e

Browse files
authored
Merge pull request microsoft#375 from TysonAndre/8.2-union-of-intersection
Support php 8.2 `A|(B&C)` dnf types
2 parents 49058c4 + 15362b1 commit a60544e

31 files changed

+2093
-72
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
/*---------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
namespace Microsoft\PhpParser\Node;
8+
9+
use Microsoft\PhpParser\MissingToken;
10+
use Microsoft\PhpParser\Node;
11+
use Microsoft\PhpParser\Node\DelimitedList\QualifiedNameList;
12+
use Microsoft\PhpParser\Token;
13+
14+
class ParenthesizedIntersectionType extends Node{
15+
/** @var Token */
16+
public $openParen;
17+
18+
/** @var QualifiedNameList|MissingToken */
19+
public $children;
20+
21+
/** @var Token */
22+
public $closeParen;
23+
24+
const CHILD_NAMES = [
25+
'openParen',
26+
'children',
27+
'closeParen'
28+
];
29+
}

src/Parser.php

Lines changed: 111 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace Microsoft\PhpParser;
88

9+
use Closure;
910
use Microsoft\PhpParser\Node\AnonymousFunctionUseClause;
1011
use Microsoft\PhpParser\Node\ArrayElement;
1112
use Microsoft\PhpParser\Node\Attribute;
@@ -70,6 +71,7 @@
7071
use Microsoft\PhpParser\Node\NamespaceAliasingClause;
7172
use Microsoft\PhpParser\Node\NamespaceUseGroupClause;
7273
use Microsoft\PhpParser\Node\NumericLiteral;
74+
use Microsoft\PhpParser\Node\ParenthesizedIntersectionType;
7375
use Microsoft\PhpParser\Node\PropertyDeclaration;
7476
use Microsoft\PhpParser\Node\ReservedWord;
7577
use Microsoft\PhpParser\Node\StringLiteral;
@@ -847,9 +849,6 @@ private function parseParameterFn() {
847849
if (end($children) instanceof MissingToken && ($children[\count($children) - 2]->kind ?? null) === TokenKind::AmpersandToken) {
848850
array_pop($parameter->typeDeclarationList->children);
849851
$parameter->byRefToken = array_pop($parameter->typeDeclarationList->children);
850-
if (!$parameter->typeDeclarationList->children) {
851-
$parameter->typeDeclarationList = null;
852-
}
853852
}
854853
} elseif ($parameter->questionToken) {
855854
// TODO ParameterType?
@@ -891,27 +890,22 @@ private function parseAndSetReturnTypeDeclarationList($parentNode) {
891890
/**
892891
* Attempt to parse the return type after the `:` and optional `?` token.
893892
*
893+
* TODO: Consider changing the return type to a new class TypeList in a future major release?
894+
* ParenthesizedIntersectionType is not a qualified name.
894895
* @return DelimitedList\QualifiedNameList|null
895896
*/
896897
private function parseReturnTypeDeclarationList($parentNode) {
897-
$result = $this->parseDelimitedList(
898-
DelimitedList\QualifiedNameList::class,
899-
self::TYPE_DELIMITER_TOKENS,
900-
function ($token) {
901-
return \in_array($token->kind, $this->returnTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token);
898+
return $this->parseUnionTypeDeclarationList(
899+
$parentNode,
900+
function ($token): bool {
901+
return \in_array($token->kind, $this->returnTypeDeclarationTokens, true) ||
902+
$this->isQualifiedNameStart($token);
902903
},
903904
function ($parentNode) {
904905
return $this->parseReturnTypeDeclaration($parentNode);
905906
},
906-
$parentNode,
907-
false);
908-
909-
// Add a MissingToken so that this will warn about `function () : T| {}`
910-
// TODO: Make this a reusable abstraction?
911-
if ($result && in_array(end($result->children)->kind ?? null, self::TYPE_DELIMITER_TOKENS)) {
912-
$result->children[] = new MissingToken(TokenKind::ReturnType, $this->token->fullStart);
913-
}
914-
return $result;
907+
TokenKind::ReturnType
908+
);
915909
}
916910

917911
private function parseReturnTypeDeclaration($parentNode) {
@@ -926,28 +920,109 @@ private function tryParseParameterTypeDeclaration($parentNode) {
926920
}
927921

928922
/**
923+
* Parse a union type such as A, A|B, A&B, A|(B&C), rejecting invalid syntax combinations.
924+
*
929925
* @param Node $parentNode
926+
* @param Closure(Token):bool $isTypeStart
927+
* @param Closure(Node):(Node|Token|null) $parseType
928+
* @param int $expectedTypeKind expected kind for token type
930929
* @return DelimitedList\QualifiedNameList|null
931930
*/
932-
private function tryParseParameterTypeDeclarationList($parentNode) {
933-
$result = $this->parseDelimitedList(
931+
private function parseUnionTypeDeclarationList($parentNode, Closure $isTypeStart, Closure $parseType, int $expectedTypeKind) {
932+
$result = new DelimitedList\QualifiedNameList();
933+
$token = $this->getCurrentToken();
934+
$delimiter = self::TYPE_DELIMITER_TOKENS;
935+
do {
936+
if ($token->kind === TokenKind::OpenParenToken || $isTypeStart($token)) {
937+
// Forbid mixing A&(B&C) if '&' was already seen
938+
$openParen = in_array(TokenKind::BarToken, $delimiter, true)
939+
? $this->eatOptional(TokenKind::OpenParenToken)
940+
: null;
941+
if ($openParen) {
942+
$element = $this->parseParenthesizedIntersectionType($result, $openParen, $isTypeStart, $parseType);
943+
// Forbid mixing (A&B)&C by forbidding `&` separator after a parenthesized intersection type.
944+
$delimiter = [TokenKind::BarToken];
945+
} else {
946+
$element = $parseType($result);
947+
}
948+
$result->addElement($element);
949+
} else {
950+
break;
951+
}
952+
953+
$delimiterToken = $this->eatOptional($delimiter);
954+
if ($delimiterToken !== null) {
955+
$result->addElement($delimiterToken);
956+
$delimiter = [$delimiterToken->kind];
957+
}
958+
$token = $this->getCurrentToken();
959+
} while ($delimiterToken !== null);
960+
961+
$result->parent = $parentNode;
962+
if ($result->children === null) {
963+
return null;
964+
}
965+
966+
if (in_array(end($result->children)->kind ?? null, $delimiter, true)) {
967+
// Add a MissingToken so that this will warn about `function () : T| {}`
968+
$result->children[] = new MissingToken($expectedTypeKind, $this->token->fullStart);
969+
} elseif (count($result->children) === 1 && $result->children[0] instanceof ParenthesizedIntersectionType) {
970+
// dnf types with parenthesized intersection types are a union type of at least 2 types.
971+
$result->children[] = new MissingToken(TokenKind::BarToken, $this->token->fullStart);
972+
}
973+
return $result;
974+
}
975+
976+
/**
977+
* @param Node $parentNode
978+
* @param Token $openParen
979+
* @param Closure(Token):bool $isTypeStart
980+
* @param Closure(Node):(Node|Token|null) $parseType
981+
*/
982+
private function parseParenthesizedIntersectionType($parentNode, Token $openParen, Closure $isTypeStart, Closure $parseType): ParenthesizedIntersectionType {
983+
$node = new ParenthesizedIntersectionType();
984+
$node->parent = $parentNode;
985+
$node->openParen = $openParen;
986+
$node->children = $this->parseDelimitedList(
934987
DelimitedList\QualifiedNameList::class,
935-
self::TYPE_DELIMITER_TOKENS,
988+
TokenKind::AmpersandToken,
989+
$isTypeStart,
990+
$parseType,
991+
$node,
992+
true);
993+
if ($node->children) {
994+
// https://wiki.php.net/rfc/dnf_types
995+
if ((end($node->children->children)->kind ?? null) === TokenKind::OpenParenToken) {
996+
// Add a MissingToken so that this will Warn about `function (A|(B&) $x) {}`
997+
$node->children->children[] = new MissingToken(TokenKind::Name, $this->token->fullStart);
998+
} elseif (count($node->children->children) === 1) {
999+
// Must have at least 2 parts for A|(B&C)
1000+
$node->children->children[] = new MissingToken(TokenKind::AmpersandToken, $this->token->fullStart);
1001+
}
1002+
} else {
1003+
// Having less than 2 types (no types) in A|() is a parse error
1004+
$node->children = new MissingToken(TokenKind::Name, $this->token->fullStart);
1005+
}
1006+
$node->closeParen = $this->eat(TokenKind::CloseParenToken);
1007+
return $node;
1008+
}
1009+
1010+
/**
1011+
* @param Node|null $parentNode
1012+
* @return DelimitedList\QualifiedNameList|null
1013+
*/
1014+
private function tryParseParameterTypeDeclarationList($parentNode) {
1015+
return $this->parseUnionTypeDeclarationList(
1016+
$parentNode,
9361017
function ($token) {
937-
return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token);
1018+
return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true) ||
1019+
$this->isQualifiedNameStart($token);
9381020
},
9391021
function ($parentNode) {
9401022
return $this->tryParseParameterTypeDeclaration($parentNode);
9411023
},
942-
$parentNode,
943-
true);
944-
945-
// Add a MissingToken so that this will Warn about `function (T| $x) {}`
946-
// TODO: Make this a reusable abstraction?
947-
if ($result && in_array(end($result->children)->kind ?? null, self::TYPE_DELIMITER_TOKENS)) {
948-
$result->children[] = new MissingToken(TokenKind::Name, $this->token->fullStart);
949-
}
950-
return $result;
1024+
TokenKind::Name
1025+
);
9511026
}
9521027

9531028
private function parseCompoundStatement($parentNode) {
@@ -959,12 +1034,6 @@ private function parseCompoundStatement($parentNode) {
9591034
return $compoundStatement;
9601035
}
9611036

962-
private function array_push_list(& $array, $list) {
963-
foreach ($list as $item) {
964-
$array[] = $item;
965-
}
966-
}
967-
9681037
private function isClassMemberDeclarationStart(Token $token) {
9691038
switch ($token->kind) {
9701039
// const-modifier
@@ -1544,6 +1613,9 @@ private function isParameterStartFn() {
15441613
case TokenKind::ProtectedKeyword:
15451614
case TokenKind::PrivateKeyword:
15461615
case TokenKind::AttributeToken:
1616+
1617+
// dnf types (A&B)|C
1618+
case TokenKind::OpenParenToken:
15471619
return true;
15481620
}
15491621

@@ -3306,6 +3378,8 @@ private function parsePropertyDeclaration($parentNode, $modifiers, $questionToke
33063378
}
33073379

33083380
/**
3381+
* Parse a comma separated qualified name list (e.g. interfaces implemented by a class)
3382+
*
33093383
* @param Node $parentNode
33103384
* @return DelimitedList\QualifiedNameList
33113385
*/
@@ -3319,6 +3393,7 @@ private function parseQualifiedNameList($parentNode) {
33193393
}
33203394

33213395
private function parseQualifiedNameCatchList($parentNode) {
3396+
// catch blocks don't support intersection types.
33223397
$result = $this->parseDelimitedList(
33233398
DelimitedList\QualifiedNameList::class,
33243399
TokenKind::BarToken,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
function foo ((A&B)|C $x): (A&B)|C|(D&E) {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

0 commit comments

Comments
 (0)