66
77namespace Microsoft \PhpParser ;
88
9+ use Closure ;
910use Microsoft \PhpParser \Node \AnonymousFunctionUseClause ;
1011use Microsoft \PhpParser \Node \ArrayElement ;
1112use Microsoft \PhpParser \Node \Attribute ;
7071use Microsoft \PhpParser \Node \NamespaceAliasingClause ;
7172use Microsoft \PhpParser \Node \NamespaceUseGroupClause ;
7273use Microsoft \PhpParser \Node \NumericLiteral ;
74+ use Microsoft \PhpParser \Node \ParenthesizedIntersectionType ;
7375use Microsoft \PhpParser \Node \PropertyDeclaration ;
7476use Microsoft \PhpParser \Node \ReservedWord ;
7577use 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,
0 commit comments