diff --git a/src/Node/DelimitedList/MatchArmConditionList.php b/src/Node/DelimitedList/MatchArmConditionList.php new file mode 100644 index 00000000..787693e4 --- /dev/null +++ b/src/Node/DelimitedList/MatchArmConditionList.php @@ -0,0 +1,12 @@ +parseQualifiedName($parentNode); } return $this->parseReservedWordExpression($parentNode); + case TokenKind::MatchKeyword: + return $this->parseMatchExpression($parentNode); } if (\in_array($token->kind, TokenStringMaps::RESERVED_WORDS)) { return $this->parseQualifiedName($parentNode); @@ -3572,6 +3577,59 @@ function ($parentNode) { return $anonymousFunctionUseClause; } + private function parseMatchExpression($parentNode) { + $matchExpression = new MatchExpression(); + $matchExpression->parent = $parentNode; + $matchExpression->matchToken = $this->eat1(TokenKind::MatchKeyword); + $matchExpression->openParen = $this->eat1(TokenKind::OpenParenToken); + $matchExpression->expression = $this->parseExpression($matchExpression); + $matchExpression->closeParen = $this->eat1(TokenKind::CloseParenToken); + $matchExpression->openBrace = $this->eat1(TokenKind::OpenBraceToken); + $matchExpression->arms = $this->parseDelimitedList( + DelimitedList\MatchExpressionArmList::class, + TokenKind::CommaToken, + $this->isMatchConditionStartFn(), + $this->parseMatchArmFn(), + $matchExpression); + $matchExpression->closeBrace = $this->eat1(TokenKind::CloseBraceToken); + return $matchExpression; + } + + private function isMatchConditionStartFn() { + return function ($token) { + return $token->kind === TokenKind::DefaultKeyword || + $this->isExpressionStart($token); + }; + } + + private function parseMatchArmFn() { + return function ($parentNode) { + $matchArm = new MatchArm(); + $matchArm->parent = $parentNode; + $matchArmConditionList = $this->parseDelimitedList( + DelimitedList\MatchArmConditionList::class, + TokenKind::CommaToken, + $this->isMatchConditionStartFn(), + $this->parseMatchConditionFn(), + $matchArm + ); + $matchArmConditionList->parent = $matchArm; + $matchArm->conditionList = $matchArmConditionList; + $matchArm->arrowToken = $this->eat1(TokenKind::DoubleArrowToken); + $matchArm->body = $this->parseExpression($matchArm); + return $matchArm; + }; + } + + private function parseMatchConditionFn() { + return function ($parentNode) { + if ($this->token->kind === TokenKind::DefaultKeyword) { + return $this->eat1(TokenKind::DefaultKeyword); + } + return $this->parseExpression($parentNode); + }; + } + private function parseCloneExpression($parentNode) { $cloneExpression = new CloneExpression(); $cloneExpression->parent = $parentNode; diff --git a/src/PhpTokenizer.php b/src/PhpTokenizer.php index 756cd159..c9af0b7d 100644 --- a/src/PhpTokenizer.php +++ b/src/PhpTokenizer.php @@ -10,6 +10,8 @@ // The replacement value is arbitrary - it just has to be different from other values of token constants. define(__NAMESPACE__ . '\T_COALESCE_EQUAL', defined('T_COALESCE_EQUAL') ? constant('T_COALESCE_EQUAL') : 'T_COALESCE_EQUAL'); define(__NAMESPACE__ . '\T_FN', defined('T_FN') ? constant('T_FN') : 'T_FN'); +// If this predaates PHP 8.0, T_MATCH is unavailable. The replacement value is arbitrary - it just has to be different from other values of token constants. +define(__NAMESPACE__ . '\T_MATCH', defined('T_MATCH') ? constant('T_MATCH') : 'T_MATCH'); /** * Tokenizes content using PHP's built-in `token_get_all`, and converts to "lightweight" Token representation. @@ -206,6 +208,7 @@ public static function getTokensArrayFromContent( T_INTERFACE => TokenKind::InterfaceKeyword, T_ISSET => TokenKind::IsSetKeyword, T_LIST => TokenKind::ListKeyword, + T_MATCH => TokenKind::MatchKeyword, T_NAMESPACE => TokenKind::NamespaceKeyword, T_NEW => TokenKind::NewKeyword, T_LOGICAL_OR => TokenKind::OrKeyword, diff --git a/src/TokenKind.php b/src/TokenKind.php index 713c318f..6207325b 100644 --- a/src/TokenKind.php +++ b/src/TokenKind.php @@ -86,6 +86,7 @@ class TokenKind { const YieldKeyword = 166; const YieldFromKeyword = 167; const FnKeyword = 168; + const MatchKeyword = 169; const OpenBracketToken = 201; const CloseBracketToken = 202; diff --git a/tests/ParserGrammarTest.php b/tests/ParserGrammarTest.php index 01a8ea59..6fff0c25 100644 --- a/tests/ParserGrammarTest.php +++ b/tests/ParserGrammarTest.php @@ -76,6 +76,7 @@ public function testOutputTreeClassificationAndLength($testCaseFile, $expectedTo const FILE_PATTERN = __DIR__ . "/cases/parser/*"; const PHP74_FILE_PATTERN = __DIR__ . "/cases/parser74/*"; + const PHP80_FILE_PATTERN = __DIR__ . "/cases/parser80/*"; public function treeProvider() { $testCases = glob(self::FILE_PATTERN . ".php"); @@ -98,6 +99,13 @@ public function treeProvider() { $testProviderArray[basename($testCase)] = [$testCase, $testCase . ".tree", $testCase . ".diag"]; } } + if (PHP_VERSION_ID >= 80000) { + $testCases = glob(self::PHP80_FILE_PATTERN . ".php"); + foreach ($testCases as $testCase) { + $testProviderArray[basename($testCase)] = [$testCase, $testCase . ".tree", $testCase . ".diag"]; + } + } + return $testProviderArray; } diff --git a/tests/cases/parser80/matchExpression1.php b/tests/cases/parser80/matchExpression1.php new file mode 100644 index 00000000..deefe0a4 --- /dev/null +++ b/tests/cases/parser80/matchExpression1.php @@ -0,0 +1,2 @@ + 0 }; diff --git a/tests/cases/parser80/matchExpression2.php.diag b/tests/cases/parser80/matchExpression2.php.diag new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/tests/cases/parser80/matchExpression2.php.diag @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/cases/parser80/matchExpression2.php.tree b/tests/cases/parser80/matchExpression2.php.tree new file mode 100644 index 00000000..53a4e80b --- /dev/null +++ b/tests/cases/parser80/matchExpression2.php.tree @@ -0,0 +1,110 @@ +{ + "SourceFileNode": { + "statementList": [ + { + "InlineHtml": { + "scriptSectionEndTag": null, + "text": null, + "scriptSectionStartTag": { + "kind": "ScriptSectionStartTag", + "textLength": 6 + } + } + }, + { + "ExpressionStatement": { + "expression": { + "AssignmentExpression": { + "leftOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + }, + "operator": { + "kind": "EqualsToken", + "textLength": 1 + }, + "byRef": null, + "rightOperand": { + "MatchExpression": { + "matchToken": { + "kind": "MatchKeyword", + "textLength": 5 + }, + "openParen": { + "kind": "OpenParenToken", + "textLength": 1 + }, + "expression": { + "NumericLiteral": { + "children": { + "kind": "IntegerLiteralToken", + "textLength": 1 + } + } + }, + "closeParen": { + "kind": "CloseParenToken", + "textLength": 1 + }, + "openBrace": { + "kind": "OpenBraceToken", + "textLength": 1 + }, + "arms": { + "MatchExpressionArmList": { + "children": [ + { + "MatchArm": { + "conditionList": { + "MatchArmConditionList": { + "children": [ + { + "kind": "DefaultKeyword", + "textLength": 7 + } + ] + } + }, + "arrowToken": { + "kind": "DoubleArrowToken", + "textLength": 2 + }, + "body": { + "NumericLiteral": { + "children": { + "kind": "IntegerLiteralToken", + "textLength": 1 + } + } + } + } + } + ] + } + }, + "closeBrace": { + "kind": "CloseBraceToken", + "textLength": 1 + } + } + } + } + }, + "semicolon": { + "kind": "SemicolonToken", + "textLength": 1 + } + } + } + ], + "endOfFileToken": { + "kind": "EndOfFileToken", + "textLength": 0 + } + } +} \ No newline at end of file diff --git a/tests/cases/parser80/matchExpression3.php b/tests/cases/parser80/matchExpression3.php new file mode 100644 index 00000000..607ea1e0 --- /dev/null +++ b/tests/cases/parser80/matchExpression3.php @@ -0,0 +1,8 @@ + $b['field'], + SOME_CONST, OTHER_CONST, + => null, +}; diff --git a/tests/cases/parser80/matchExpression3.php.diag b/tests/cases/parser80/matchExpression3.php.diag new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/tests/cases/parser80/matchExpression3.php.diag @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/cases/parser80/matchExpression3.php.tree b/tests/cases/parser80/matchExpression3.php.tree new file mode 100644 index 00000000..25cf799e --- /dev/null +++ b/tests/cases/parser80/matchExpression3.php.tree @@ -0,0 +1,199 @@ +{ + "SourceFileNode": { + "statementList": [ + { + "InlineHtml": { + "scriptSectionEndTag": null, + "text": null, + "scriptSectionStartTag": { + "kind": "ScriptSectionStartTag", + "textLength": 6 + } + } + }, + { + "ExpressionStatement": { + "expression": { + "AssignmentExpression": { + "leftOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + }, + "operator": { + "kind": "EqualsToken", + "textLength": 1 + }, + "byRef": null, + "rightOperand": { + "MatchExpression": { + "matchToken": { + "kind": "MatchKeyword", + "textLength": 5 + }, + "openParen": { + "kind": "OpenParenToken", + "textLength": 1 + }, + "expression": { + "NumericLiteral": { + "children": { + "kind": "IntegerLiteralToken", + "textLength": 1 + } + } + }, + "closeParen": { + "kind": "CloseParenToken", + "textLength": 1 + }, + "openBrace": { + "kind": "OpenBraceToken", + "textLength": 1 + }, + "arms": { + "MatchExpressionArmList": { + "children": [ + { + "MatchArm": { + "conditionList": { + "MatchArmConditionList": { + "children": [ + { + "kind": "DefaultKeyword", + "textLength": 7 + }, + { + "kind": "CommaToken", + "textLength": 1 + } + ] + } + }, + "arrowToken": { + "kind": "DoubleArrowToken", + "textLength": 2 + }, + "body": { + "SubscriptExpression": { + "postfixExpression": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + }, + "openBracketOrBrace": { + "kind": "OpenBracketToken", + "textLength": 1 + }, + "accessExpression": { + "StringLiteral": { + "startQuote": null, + "children": { + "kind": "StringLiteralToken", + "textLength": 7 + }, + "endQuote": null + } + }, + "closeBracketOrBrace": { + "kind": "CloseBracketToken", + "textLength": 1 + } + } + } + } + }, + { + "kind": "CommaToken", + "textLength": 1 + }, + { + "MatchArm": { + "conditionList": { + "MatchArmConditionList": { + "children": [ + { + "QualifiedName": { + "globalSpecifier": null, + "relativeSpecifier": null, + "nameParts": [ + { + "kind": "Name", + "textLength": 10 + } + ] + } + }, + { + "kind": "CommaToken", + "textLength": 1 + }, + { + "QualifiedName": { + "globalSpecifier": null, + "relativeSpecifier": null, + "nameParts": [ + { + "kind": "Name", + "textLength": 11 + } + ] + } + }, + { + "kind": "CommaToken", + "textLength": 1 + } + ] + } + }, + "arrowToken": { + "kind": "DoubleArrowToken", + "textLength": 2 + }, + "body": { + "ReservedWord": { + "children": { + "kind": "NullReservedWord", + "textLength": 4 + } + } + } + } + }, + { + "kind": "CommaToken", + "textLength": 1 + } + ] + } + }, + "closeBrace": { + "kind": "CloseBraceToken", + "textLength": 1 + } + } + } + } + }, + "semicolon": { + "kind": "SemicolonToken", + "textLength": 1 + } + } + } + ], + "endOfFileToken": { + "kind": "EndOfFileToken", + "textLength": 0 + } + } +} \ No newline at end of file diff --git a/tests/cases/parser80/matchExpression4.php b/tests/cases/parser80/matchExpression4.php new file mode 100644 index 00000000..a02c52ec --- /dev/null +++ b/tests/cases/parser80/matchExpression4.php @@ -0,0 +1,7 @@ + get_letters(), + 0, => match (true) { + $a == $b => 'zero', + }, +}; diff --git a/tests/cases/parser80/matchExpression4.php.diag b/tests/cases/parser80/matchExpression4.php.diag new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/tests/cases/parser80/matchExpression4.php.diag @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/cases/parser80/matchExpression4.php.tree b/tests/cases/parser80/matchExpression4.php.tree new file mode 100644 index 00000000..83420686 --- /dev/null +++ b/tests/cases/parser80/matchExpression4.php.tree @@ -0,0 +1,278 @@ +{ + "SourceFileNode": { + "statementList": [ + { + "InlineHtml": { + "scriptSectionEndTag": null, + "text": null, + "scriptSectionStartTag": { + "kind": "ScriptSectionStartTag", + "textLength": 6 + } + } + }, + { + "ExpressionStatement": { + "expression": { + "MatchExpression": { + "matchToken": { + "kind": "MatchKeyword", + "textLength": 5 + }, + "openParen": { + "kind": "OpenParenToken", + "textLength": 1 + }, + "expression": { + "SubscriptExpression": { + "postfixExpression": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + }, + "openBracketOrBrace": { + "kind": "OpenBracketToken", + "textLength": 1 + }, + "accessExpression": { + "StringLiteral": { + "startQuote": null, + "children": { + "kind": "StringLiteralToken", + "textLength": 7 + }, + "endQuote": null + } + }, + "closeBracketOrBrace": { + "kind": "CloseBracketToken", + "textLength": 1 + } + } + }, + "closeParen": { + "kind": "CloseParenToken", + "textLength": 1 + }, + "openBrace": { + "kind": "OpenBraceToken", + "textLength": 1 + }, + "arms": { + "MatchExpressionArmList": { + "children": [ + { + "MatchArm": { + "conditionList": { + "MatchArmConditionList": { + "children": [ + { + "StringLiteral": { + "startQuote": null, + "children": { + "kind": "StringLiteralToken", + "textLength": 3 + }, + "endQuote": null + } + }, + { + "kind": "CommaToken", + "textLength": 1 + }, + { + "StringLiteral": { + "startQuote": null, + "children": { + "kind": "StringLiteralToken", + "textLength": 3 + }, + "endQuote": null + } + } + ] + } + }, + "arrowToken": { + "kind": "DoubleArrowToken", + "textLength": 2 + }, + "body": { + "CallExpression": { + "callableExpression": { + "QualifiedName": { + "globalSpecifier": null, + "relativeSpecifier": null, + "nameParts": [ + { + "kind": "Name", + "textLength": 11 + } + ] + } + }, + "openParen": { + "kind": "OpenParenToken", + "textLength": 1 + }, + "argumentExpressionList": null, + "closeParen": { + "kind": "CloseParenToken", + "textLength": 1 + } + } + } + } + }, + { + "kind": "CommaToken", + "textLength": 1 + }, + { + "MatchArm": { + "conditionList": { + "MatchArmConditionList": { + "children": [ + { + "NumericLiteral": { + "children": { + "kind": "IntegerLiteralToken", + "textLength": 1 + } + } + }, + { + "kind": "CommaToken", + "textLength": 1 + } + ] + } + }, + "arrowToken": { + "kind": "DoubleArrowToken", + "textLength": 2 + }, + "body": { + "MatchExpression": { + "matchToken": { + "kind": "MatchKeyword", + "textLength": 5 + }, + "openParen": { + "kind": "OpenParenToken", + "textLength": 1 + }, + "expression": { + "ReservedWord": { + "children": { + "kind": "TrueReservedWord", + "textLength": 4 + } + } + }, + "closeParen": { + "kind": "CloseParenToken", + "textLength": 1 + }, + "openBrace": { + "kind": "OpenBraceToken", + "textLength": 1 + }, + "arms": { + "MatchExpressionArmList": { + "children": [ + { + "MatchArm": { + "conditionList": { + "MatchArmConditionList": { + "children": [ + { + "BinaryExpression": { + "leftOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + }, + "operator": { + "kind": "EqualsEqualsToken", + "textLength": 2 + }, + "rightOperand": { + "Variable": { + "dollar": null, + "name": { + "kind": "VariableName", + "textLength": 2 + } + } + } + } + } + ] + } + }, + "arrowToken": { + "kind": "DoubleArrowToken", + "textLength": 2 + }, + "body": { + "StringLiteral": { + "startQuote": null, + "children": { + "kind": "StringLiteralToken", + "textLength": 6 + }, + "endQuote": null + } + } + } + }, + { + "kind": "CommaToken", + "textLength": 1 + } + ] + } + }, + "closeBrace": { + "kind": "CloseBraceToken", + "textLength": 1 + } + } + } + } + }, + { + "kind": "CommaToken", + "textLength": 1 + } + ] + } + }, + "closeBrace": { + "kind": "CloseBraceToken", + "textLength": 1 + } + } + }, + "semicolon": { + "kind": "SemicolonToken", + "textLength": 1 + } + } + } + ], + "endOfFileToken": { + "kind": "EndOfFileToken", + "textLength": 0 + } + } +} \ No newline at end of file diff --git a/tests/cases/parser80/matchExpression5.php b/tests/cases/parser80/matchExpression5.php new file mode 100644 index 00000000..7e89e8c4 --- /dev/null +++ b/tests/cases/parser80/matchExpression5.php @@ -0,0 +1,3 @@ + }; diff --git a/tests/cases/parser80/matchExpression5.php.diag b/tests/cases/parser80/matchExpression5.php.diag new file mode 100644 index 00000000..7300906a --- /dev/null +++ b/tests/cases/parser80/matchExpression5.php.diag @@ -0,0 +1,14 @@ +[ + { + "kind": 0, + "message": "'Expression' expected.", + "start": 40, + "length": 0 + }, + { + "kind": 0, + "message": "'Expression' expected.", + "start": 54, + "length": 0 + } +] \ No newline at end of file diff --git a/tests/cases/parser80/matchExpression5.php.tree b/tests/cases/parser80/matchExpression5.php.tree new file mode 100644 index 00000000..d13154b9 --- /dev/null +++ b/tests/cases/parser80/matchExpression5.php.tree @@ -0,0 +1,106 @@ +{ + "SourceFileNode": { + "statementList": [ + { + "InlineHtml": { + "scriptSectionEndTag": null, + "text": null, + "scriptSectionStartTag": { + "kind": "ScriptSectionStartTag", + "textLength": 6 + } + } + }, + { + "ExpressionStatement": { + "expression": { + "EchoExpression": { + "echoKeyword": { + "kind": "EchoKeyword", + "textLength": 4 + }, + "expressions": { + "ExpressionList": { + "children": [ + { + "MatchExpression": { + "matchToken": { + "kind": "MatchKeyword", + "textLength": 5 + }, + "openParen": { + "kind": "OpenParenToken", + "textLength": 1 + }, + "expression": { + "error": "MissingToken", + "kind": "Expression", + "textLength": 0 + }, + "closeParen": { + "kind": "CloseParenToken", + "textLength": 1 + }, + "openBrace": { + "kind": "OpenBraceToken", + "textLength": 1 + }, + "arms": { + "MatchExpressionArmList": { + "children": [ + { + "MatchArm": { + "conditionList": { + "MatchArmConditionList": { + "children": [ + { + "StringLiteral": { + "startQuote": null, + "children": { + "kind": "StringLiteralToken", + "textLength": 7 + }, + "endQuote": null + } + } + ] + } + }, + "arrowToken": { + "kind": "DoubleArrowToken", + "textLength": 2 + }, + "body": { + "error": "MissingToken", + "kind": "Expression", + "textLength": 0 + } + } + } + ] + } + }, + "closeBrace": { + "kind": "CloseBraceToken", + "textLength": 1 + } + } + } + ] + } + } + } + }, + "semicolon": { + "kind": "SemicolonToken", + "textLength": 1 + } + } + } + ], + "endOfFileToken": { + "kind": "EndOfFileToken", + "textLength": 0 + } + } +} \ No newline at end of file