Skip to content

Commit bcdd1a1

Browse files
committed
Support php 8.2 A|(B&C) dnf types
Support php 8.2's Disjunctive Normal Form Types (allowing parenthesized intersection types of 2 or more types in union types) https://wiki.php.net/rfc/dnf_types Closes microsoft#372
1 parent 9bf6637 commit bcdd1a1

17 files changed

+1050
-9
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: 73 additions & 9 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;
@@ -894,13 +896,31 @@ private function parseAndSetReturnTypeDeclarationList($parentNode) {
894896
* @return DelimitedList\QualifiedNameList|null
895897
*/
896898
private function parseReturnTypeDeclarationList($parentNode) {
899+
// TODO: Forbid mixing `|` and `&` in another PR, that's a parse error.
900+
// TODO: Forbid mixing (A&B)&C in another PR, that's a parse error.
897901
$result = $this->parseDelimitedList(
898902
DelimitedList\QualifiedNameList::class,
899903
self::TYPE_DELIMITER_TOKENS,
900904
function ($token) {
901-
return \in_array($token->kind, $this->returnTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token);
905+
return \in_array($token->kind, $this->returnTypeDeclarationTokens, true) ||
906+
$token->kind === TokenKind::OpenParenToken ||
907+
$this->isQualifiedNameStart($token);
902908
},
903909
function ($parentNode) {
910+
$openParen = $this->eatOptional(TokenKind::OpenParenToken);
911+
if ($openParen) {
912+
return $this->parseParenthesizedIntersectionType(
913+
$parentNode,
914+
$openParen,
915+
function ($token) {
916+
return \in_array($token->kind, $this->returnTypeDeclarationTokens, true) ||
917+
$this->isQualifiedNameStart($token);
918+
},
919+
function ($parentNode) {
920+
return $this->parseReturnTypeDeclaration($parentNode);
921+
}
922+
);
923+
}
904924
return $this->parseReturnTypeDeclaration($parentNode);
905925
},
906926
$parentNode,
@@ -925,18 +945,62 @@ private function tryParseParameterTypeDeclaration($parentNode) {
925945
return $parameterTypeDeclaration;
926946
}
927947

948+
private function parseParenthesizedIntersectionType($parentNode, Token $openParen, Closure $isTypeStart, Closure $parseType): ParenthesizedIntersectionType {
949+
$node = new ParenthesizedIntersectionType();
950+
$node->parent = $parentNode;
951+
$node->openParen = $openParen;
952+
$node->children = $this->parseDelimitedList(
953+
DelimitedList\QualifiedNameList::class,
954+
TokenKind::AmpersandToken,
955+
$isTypeStart,
956+
$parseType,
957+
$node,
958+
true);
959+
if ($node->children) {
960+
// https://wiki.php.net/rfc/dnf_types
961+
if ((end($node->children->children)->kind ?? null) === TokenKind::OpenParenToken) {
962+
// Add a MissingToken so that this will Warn about `function (A|(B&) $x) {}`
963+
$node->children->children[] = new MissingToken(TokenKind::Name, $this->token->fullStart);
964+
} elseif (count($node->children->children) === 1) {
965+
// Must have at least 2 parts for A|(B&C)
966+
$node->children->children[] = new MissingToken(TokenKind::AmpersandToken, $this->token->fullStart);
967+
}
968+
} else {
969+
// Having less than 2 types (no types) in A|() is a parse error
970+
$node->children = new MissingToken(TokenKind::Name, $this->token->fullStart);
971+
}
972+
$node->closeParen = $this->eat(TokenKind::CloseParenToken);
973+
return $node;
974+
}
975+
928976
/**
929-
* @param Node $parentNode
977+
* @param Node|null $parentNode
930978
* @return DelimitedList\QualifiedNameList|null
931979
*/
932980
private function tryParseParameterTypeDeclarationList($parentNode) {
933981
$result = $this->parseDelimitedList(
934982
DelimitedList\QualifiedNameList::class,
935983
self::TYPE_DELIMITER_TOKENS,
936984
function ($token) {
937-
return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token);
985+
return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true) ||
986+
$token->kind === TokenKind::OpenParenToken ||
987+
$this->isQualifiedNameStart($token);
938988
},
939989
function ($parentNode) {
990+
$openParen = $this->eatOptional(TokenKind::OpenParenToken);
991+
if ($openParen) {
992+
return $this->parseParenthesizedIntersectionType(
993+
$parentNode,
994+
$openParen,
995+
function ($token) {
996+
return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true) ||
997+
$this->isQualifiedNameStart($token);
998+
},
999+
function ($parentNode) {
1000+
return $this->tryParseParameterTypeDeclaration($parentNode);
1001+
}
1002+
);
1003+
}
9401004
return $this->tryParseParameterTypeDeclaration($parentNode);
9411005
},
9421006
$parentNode,
@@ -959,12 +1023,6 @@ private function parseCompoundStatement($parentNode) {
9591023
return $compoundStatement;
9601024
}
9611025

962-
private function array_push_list(& $array, $list) {
963-
foreach ($list as $item) {
964-
$array[] = $item;
965-
}
966-
}
967-
9681026
private function isClassMemberDeclarationStart(Token $token) {
9691027
switch ($token->kind) {
9701028
// const-modifier
@@ -1544,6 +1602,9 @@ private function isParameterStartFn() {
15441602
case TokenKind::ProtectedKeyword:
15451603
case TokenKind::PrivateKeyword:
15461604
case TokenKind::AttributeToken:
1605+
1606+
// dnf types (A&B)|C
1607+
case TokenKind::OpenParenToken:
15471608
return true;
15481609
}
15491610

@@ -3306,6 +3367,8 @@ private function parsePropertyDeclaration($parentNode, $modifiers, $questionToke
33063367
}
33073368

33083369
/**
3370+
* Parse a comma separated qualified name list (e.g. interfaces implemented by a class)
3371+
*
33093372
* @param Node $parentNode
33103373
* @return DelimitedList\QualifiedNameList
33113374
*/
@@ -3319,6 +3382,7 @@ private function parseQualifiedNameList($parentNode) {
33193382
}
33203383

33213384
private function parseQualifiedNameCatchList($parentNode) {
3385+
// catch blocks don't support intersection types.
33223386
$result = $this->parseDelimitedList(
33233387
DelimitedList\QualifiedNameList::class,
33243388
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)