Files
core/vendor/microsoft/tolerant-php-parser/src/Parser.php
T
2020-05-01 15:26:54 +09:00

3607 lines
153 KiB
PHP

<?php
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
namespace Microsoft\PhpParser;
use Microsoft\PhpParser\Node\AnonymousFunctionUseClause;
use Microsoft\PhpParser\Node\ArrayElement;
use Microsoft\PhpParser\Node\CaseStatementNode;
use Microsoft\PhpParser\Node\CatchClause;
use Microsoft\PhpParser\Node\ClassBaseClause;
use Microsoft\PhpParser\Node\ClassInterfaceClause;
use Microsoft\PhpParser\Node\ClassMembersNode;
use Microsoft\PhpParser\Node\ConstElement;
use Microsoft\PhpParser\Node\Expression;
use Microsoft\PhpParser\Node\Expression\{
AnonymousFunctionCreationExpression,
ArgumentExpression,
ArrayCreationExpression,
ArrowFunctionCreationExpression,
AssignmentExpression,
BinaryExpression,
BracedExpression,
CallExpression,
CastExpression,
CloneExpression,
EmptyIntrinsicExpression,
ErrorControlExpression,
EvalIntrinsicExpression,
ExitIntrinsicExpression,
IssetIntrinsicExpression,
MemberAccessExpression,
ParenthesizedExpression,
PrefixUpdateExpression,
PrintIntrinsicExpression,
EchoExpression,
ListIntrinsicExpression,
ObjectCreationExpression,
ScriptInclusionExpression,
PostfixUpdateExpression,
ScopedPropertyAccessExpression,
SubscriptExpression,
TernaryExpression,
UnaryExpression,
UnaryOpExpression,
UnsetIntrinsicExpression,
Variable,
YieldExpression
};
use Microsoft\PhpParser\Node\StaticVariableDeclaration;
use Microsoft\PhpParser\Node\ClassConstDeclaration;
use Microsoft\PhpParser\Node\DeclareDirective;
use Microsoft\PhpParser\Node\DelimitedList;
use Microsoft\PhpParser\Node\ElseClauseNode;
use Microsoft\PhpParser\Node\ElseIfClauseNode;
use Microsoft\PhpParser\Node\FinallyClause;
use Microsoft\PhpParser\Node\ForeachKey;
use Microsoft\PhpParser\Node\ForeachValue;
use Microsoft\PhpParser\Node\InterfaceBaseClause;
use Microsoft\PhpParser\Node\InterfaceMembers;
use Microsoft\PhpParser\Node\MissingMemberDeclaration;
use Microsoft\PhpParser\Node\NamespaceAliasingClause;
use Microsoft\PhpParser\Node\NamespaceUseGroupClause;
use Microsoft\PhpParser\Node\NumericLiteral;
use Microsoft\PhpParser\Node\PropertyDeclaration;
use Microsoft\PhpParser\Node\ReservedWord;
use Microsoft\PhpParser\Node\StringLiteral;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\Parameter;
use Microsoft\PhpParser\Node\QualifiedName;
use Microsoft\PhpParser\Node\RelativeSpecifier;
use Microsoft\PhpParser\Node\SourceFileNode;
use Microsoft\PhpParser\Node\Statement\{
ClassDeclaration,
ConstDeclaration,
CompoundStatementNode,
FunctionStaticDeclaration,
GlobalDeclaration,
BreakOrContinueStatement,
DeclareStatement,
DoStatement,
EmptyStatement,
ExpressionStatement,
ForeachStatement,
ForStatement,
FunctionDeclaration,
GotoStatement,
IfStatementNode,
InlineHtml,
InterfaceDeclaration,
NamespaceDefinition,
NamespaceUseDeclaration,
NamedLabelStatement,
ReturnStatement,
SwitchStatementNode,
ThrowStatement,
TraitDeclaration,
TryStatement,
WhileStatement
};
use Microsoft\PhpParser\Node\TraitMembers;
use Microsoft\PhpParser\Node\TraitSelectOrAliasClause;
use Microsoft\PhpParser\Node\TraitUseClause;
use Microsoft\PhpParser\Node\UseVariableName;
use Microsoft\PhpParser\Node\NamespaceUseClause;
class Parser {
/** @var TokenStreamProviderInterface */
private $lexer;
private $currentParseContext;
public $sourceFile;
private $nameOrKeywordOrReservedWordTokens;
private $nameOrReservedWordTokens;
private $nameOrStaticOrReservedWordTokens;
private $reservedWordTokens;
private $keywordTokens;
// TODO consider validating parameter and return types on post-parse instead so we can be more permissive
private $parameterTypeDeclarationTokens;
private $returnTypeDeclarationTokens;
public function __construct() {
$this->reservedWordTokens = \array_values(TokenStringMaps::RESERVED_WORDS);
$this->keywordTokens = \array_values(TokenStringMaps::KEYWORDS);
$this->nameOrKeywordOrReservedWordTokens = \array_merge([TokenKind::Name], $this->keywordTokens, $this->reservedWordTokens);
$this->nameOrReservedWordTokens = \array_merge([TokenKind::Name], $this->reservedWordTokens);
$this->nameOrStaticOrReservedWordTokens = \array_merge([TokenKind::Name, TokenKind::StaticKeyword], $this->reservedWordTokens);
$this->parameterTypeDeclarationTokens =
[TokenKind::ArrayKeyword, TokenKind::CallableKeyword, TokenKind::BoolReservedWord,
TokenKind::FloatReservedWord, TokenKind::IntReservedWord, TokenKind::StringReservedWord,
TokenKind::ObjectReservedWord, TokenKind::NullReservedWord, TokenKind::FalseReservedWord]; // TODO update spec
$this->returnTypeDeclarationTokens = \array_merge([TokenKind::VoidReservedWord, TokenKind::NullReservedWord, TokenKind::FalseReservedWord, TokenKind::StaticKeyword], $this->parameterTypeDeclarationTokens);
}
/**
* Generates AST from source file contents. Returns an instance of SourceFileNode, which is always the top-most
* Node-type of the tree.
*
* @param string $fileContents
* @return SourceFileNode
*/
public function parseSourceFile(string $fileContents, string $uri = null) : SourceFileNode {
$this->lexer = TokenStreamProviderFactory::GetTokenStreamProvider($fileContents);
$this->reset();
$sourceFile = new SourceFileNode();
$this->sourceFile = $sourceFile;
$sourceFile->fileContents = $fileContents;
$sourceFile->uri = $uri;
$sourceFile->statementList = array();
if ($this->getCurrentToken()->kind !== TokenKind::EndOfFileToken) {
$inlineHTML = $this->parseInlineHtml($sourceFile);
$sourceFile->statementList[] = $inlineHTML;
if ($inlineHTML->echoStatement) {
$sourceFile->statementList[] = $inlineHTML->echoStatement;
$inlineHTML->echoStatement->parent = $sourceFile;
$inlineHTML->echoStatement = null;
}
}
$sourceFile->statementList =
\array_merge($sourceFile->statementList, $this->parseList($sourceFile, ParseContext::SourceElements));
$this->sourceFile->endOfFileToken = $this->eat1(TokenKind::EndOfFileToken);
$this->advanceToken();
$sourceFile->parent = null;
return $sourceFile;
}
private function reset() {
$this->advanceToken();
// Stores the current parse context, which includes the current and enclosing lists.
$this->currentParseContext = 0;
}
/**
* Parse a list of elements for a given ParseContext until a list terminator associated
* with that ParseContext is reached. Additionally abort parsing when an element is reached
* that is invalid in the current context, but valid in an enclosing context. If an element
* is invalid in both current and enclosing contexts, generate a SkippedToken, and continue.
* @param Node $parentNode
* @param int $listParseContext
* @return array
*/
private function parseList($parentNode, int $listParseContext) {
$savedParseContext = $this->currentParseContext;
$this->currentParseContext |= 1 << $listParseContext;
$parseListElementFn = $this->getParseListElementFn($listParseContext);
$nodeArray = array();
while (!$this->isListTerminator($listParseContext)) {
if ($this->isValidListElement($listParseContext, $this->getCurrentToken())) {
$element = $parseListElementFn($parentNode);
$nodeArray[] = $element;
if ($element instanceof Node) {
$element->parent = $parentNode;
if ($element instanceof InlineHtml && $element->echoStatement) {
$nodeArray[] = $element->echoStatement;
$element->echoStatement->parent = $parentNode;
$element->echoStatement = null;
}
}
continue;
}
// Error handling logic:
// The current parse context does not know how to handle the current token,
// so check if the enclosing contexts know what to do. If so, we assume that
// the list has completed parsing, and return to the enclosing context.
//
// Example:
// class A {
// function foo() {
// return;
// // } <- MissingToken (generated when we try to "eat" the closing brace)
//
// public function bar() {
// }
// }
//
// In the case above, the Method ParseContext doesn't know how to handle "public", but
// the Class ParseContext will know what to do with it. So we abort the Method ParseContext,
// and return to the Class ParseContext. This enables us to generate a tree with a single
// class that contains two method nodes, even though there was an error present in the first method.
if ($this->isCurrentTokenValidInEnclosingContexts()) {
break;
}
// None of the enclosing contexts know how to handle the token. Generate a
// SkippedToken, and continue parsing in the current context.
// Example:
// class A {
// function foo() {
// return;
// & // <- SkippedToken
// }
// }
$token = new SkippedToken($this->getCurrentToken());
$nodeArray[] = $token;
$this->advanceToken();
}
$this->currentParseContext = $savedParseContext;
return $nodeArray;
}
private function isListTerminator(int $parseContext) {
$tokenKind = $this->getCurrentToken()->kind;
if ($tokenKind === TokenKind::EndOfFileToken) {
// Being at the end of the file ends all lists.
return true;
}
switch ($parseContext) {
case ParseContext::SourceElements:
return false;
case ParseContext::InterfaceMembers:
case ParseContext::ClassMembers:
case ParseContext::BlockStatements:
case ParseContext::TraitMembers:
return $tokenKind === TokenKind::CloseBraceToken;
case ParseContext::SwitchStatementElements:
return $tokenKind === TokenKind::CloseBraceToken || $tokenKind === TokenKind::EndSwitchKeyword;
case ParseContext::IfClause2Elements:
return
$tokenKind === TokenKind::ElseIfKeyword ||
$tokenKind === TokenKind::ElseKeyword ||
$tokenKind === TokenKind::EndIfKeyword;
case ParseContext::WhileStatementElements:
return $tokenKind === TokenKind::EndWhileKeyword;
case ParseContext::CaseStatementElements:
return
$tokenKind === TokenKind::CaseKeyword ||
$tokenKind === TokenKind::DefaultKeyword;
case ParseContext::ForStatementElements:
return
$tokenKind === TokenKind::EndForKeyword;
case ParseContext::ForeachStatementElements:
return $tokenKind === TokenKind::EndForEachKeyword;
case ParseContext::DeclareStatementElements:
return $tokenKind === TokenKind::EndDeclareKeyword;
}
// TODO warn about unhandled parse context
return false;
}
private function isValidListElement($context, Token $token) {
// TODO
switch ($context) {
case ParseContext::SourceElements:
case ParseContext::BlockStatements:
case ParseContext::IfClause2Elements:
case ParseContext::CaseStatementElements:
case ParseContext::WhileStatementElements:
case ParseContext::ForStatementElements:
case ParseContext::ForeachStatementElements:
case ParseContext::DeclareStatementElements:
return $this->isStatementStart($token);
case ParseContext::ClassMembers:
return $this->isClassMemberDeclarationStart($token);
case ParseContext::TraitMembers:
return $this->isTraitMemberDeclarationStart($token);
case ParseContext::InterfaceMembers:
return $this->isInterfaceMemberDeclarationStart($token);
case ParseContext::SwitchStatementElements:
return
$token->kind === TokenKind::CaseKeyword ||
$token->kind === TokenKind::DefaultKeyword;
}
return false;
}
private function getParseListElementFn($context) {
switch ($context) {
case ParseContext::SourceElements:
case ParseContext::BlockStatements:
case ParseContext::IfClause2Elements:
case ParseContext::CaseStatementElements:
case ParseContext::WhileStatementElements:
case ParseContext::ForStatementElements:
case ParseContext::ForeachStatementElements:
case ParseContext::DeclareStatementElements:
return $this->parseStatementFn();
case ParseContext::ClassMembers:
return $this->parseClassElementFn();
case ParseContext::TraitMembers:
return $this->parseTraitElementFn();
case ParseContext::InterfaceMembers:
return $this->parseInterfaceElementFn();
case ParseContext::SwitchStatementElements:
return $this->parseCaseOrDefaultStatement();
default:
throw new \Exception("Unrecognized parse context");
}
}
/**
* Aborts parsing list when one of the parent contexts understands something
* @return bool
*/
private function isCurrentTokenValidInEnclosingContexts() {
for ($contextKind = 0; $contextKind < ParseContext::Count; $contextKind++) {
if ($this->isInParseContext($contextKind)) {
if ($this->isValidListElement($contextKind, $this->getCurrentToken()) || $this->isListTerminator($contextKind)) {
return true;
}
}
}
return false;
}
private function isInParseContext($contextToCheck) {
return ($this->currentParseContext & (1 << $contextToCheck));
}
/**
* Retrieve the current token, and check that it's of the expected TokenKind.
* If so, advance and return the token. Otherwise return a MissingToken for
* the expected token.
* @param int|int[] ...$kinds
* @return Token
*/
private function eat(...$kinds) {
$token = $this->token;
if (\is_array($kinds[0])) {
$kinds = $kinds[0];
}
foreach ($kinds as $kind) {
if ($token->kind === $kind) {
$this->token = $this->lexer->scanNextToken();
return $token;
}
}
// TODO include optional grouping for token kinds
return new MissingToken($kinds[0], $token->fullStart);
}
/**
* Retrieve the current token, and check that it's of the kind $kind.
* If so, advance and return the token. Otherwise return a MissingToken for
* the expected token.
*
* This is faster than calling eat() if there is a single token.
*
* @param int $kind
* @return Token
*/
private function eat1($kind) {
$token = $this->token;
if ($token->kind === $kind) {
$this->token = $this->lexer->scanNextToken();
return $token;
}
// TODO include optional grouping for token kinds
return new MissingToken($kind, $token->fullStart);
}
/**
* @param int|int[] ...$kinds (Can provide a single value with a list of kinds, or multiple kinds)
* @return Token|null
*/
private function eatOptional(...$kinds) {
$token = $this->token;
if (\is_array($kinds[0])) {
$kinds = $kinds[0];
}
if (\in_array($token->kind, $kinds)) {
$this->token = $this->lexer->scanNextToken();
return $token;
}
return null;
}
/**
* @param int $kind a single kind
* @return Token|null
*/
private function eatOptional1($kind) {
$token = $this->token;
if ($token->kind === $kind) {
$this->token = $this->lexer->scanNextToken();
return $token;
}
return null;
}
private $token;
private function getCurrentToken() : Token {
return $this->token;
}
private function advanceToken() {
$this->token = $this->lexer->scanNextToken();
}
private function parseStatement($parentNode) {
return ($this->parseStatementFn())($parentNode);
}
private function parseStatementFn() {
return function ($parentNode) {
$token = $this->getCurrentToken();
switch ($token->kind) {
// compound-statement
case TokenKind::OpenBraceToken:
return $this->parseCompoundStatement($parentNode);
// labeled-statement
case TokenKind::Name:
if ($this->lookahead(TokenKind::ColonToken)) {
return $this->parseNamedLabelStatement($parentNode);
}
break;
// selection-statement
case TokenKind::IfKeyword:
return $this->parseIfStatement($parentNode);
case TokenKind::SwitchKeyword:
return $this->parseSwitchStatement($parentNode);
// iteration-statement
case TokenKind::WhileKeyword: // while-statement
return $this->parseWhileStatement($parentNode);
case TokenKind::DoKeyword: // do-statement
return $this->parseDoStatement($parentNode);
case TokenKind::ForKeyword: // for-statement
return $this->parseForStatement($parentNode);
case TokenKind::ForeachKeyword: // foreach-statement
return $this->parseForeachStatement($parentNode);
// jump-statement
case TokenKind::GotoKeyword: // goto-statement
return $this->parseGotoStatement($parentNode);
case TokenKind::ContinueKeyword: // continue-statement
case TokenKind::BreakKeyword: // break-statement
return $this->parseBreakOrContinueStatement($parentNode);
case TokenKind::ReturnKeyword: // return-statement
return $this->parseReturnStatement($parentNode);
case TokenKind::ThrowKeyword: // throw-statement
return $this->parseThrowStatement($parentNode);
// try-statement
case TokenKind::TryKeyword:
return $this->parseTryStatement($parentNode);
// declare-statement
case TokenKind::DeclareKeyword:
return $this->parseDeclareStatement($parentNode);
// function-declaration
case TokenKind::FunctionKeyword:
// Check that this is not an anonymous-function-creation-expression
if ($this->lookahead($this->nameOrKeywordOrReservedWordTokens) || $this->lookahead(TokenKind::AmpersandToken, $this->nameOrKeywordOrReservedWordTokens)) {
return $this->parseFunctionDeclaration($parentNode);
}
break;
// class-declaration
case TokenKind::FinalKeyword:
case TokenKind::AbstractKeyword:
if (!$this->lookahead(TokenKind::ClassKeyword)) {
$this->advanceToken();
return new SkippedToken($token);
}
case TokenKind::ClassKeyword:
return $this->parseClassDeclaration($parentNode);
// interface-declaration
case TokenKind::InterfaceKeyword:
return $this->parseInterfaceDeclaration($parentNode);
// namespace-definition
case TokenKind::NamespaceKeyword:
if (!$this->lookahead(TokenKind::BackslashToken)) {
// TODO add error handling for the case where a namespace definition does not occur in the outer-most scope
return $this->parseNamespaceDefinition($parentNode);
}
break;
// namespace-use-declaration
case TokenKind::UseKeyword:
return $this->parseNamespaceUseDeclaration($parentNode);
case TokenKind::SemicolonToken:
return $this->parseEmptyStatement($parentNode);
case TokenKind::EchoKeyword:
return $this->parseEchoStatement($parentNode);
// trait-declaration
case TokenKind::TraitKeyword:
return $this->parseTraitDeclaration($parentNode);
// global-declaration
case TokenKind::GlobalKeyword:
return $this->parseGlobalDeclaration($parentNode);
// const-declaration
case TokenKind::ConstKeyword:
return $this->parseConstDeclaration($parentNode);
// function-static-declaration
case TokenKind::StaticKeyword:
// Check that this is not an anonymous-function-creation-expression
if (!$this->lookahead([TokenKind::FunctionKeyword, TokenKind::FnKeyword, TokenKind::OpenParenToken, TokenKind::ColonColonToken])) {
return $this->parseFunctionStaticDeclaration($parentNode);
}
break;
case TokenKind::ScriptSectionEndTag:
return $this->parseInlineHtml($parentNode);
case TokenKind::UnsetKeyword:
return $this->parseUnsetStatement($parentNode);
}
$expressionStatement = new ExpressionStatement();
$expressionStatement->parent = $parentNode;
$expressionStatement->expression = $this->parseExpression($expressionStatement, true);
$expressionStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $expressionStatement;
};
}
private function parseClassElementFn() {
return function ($parentNode) {
$modifiers = $this->parseModifiers();
$token = $this->getCurrentToken();
switch ($token->kind) {
case TokenKind::ConstKeyword:
return $this->parseClassConstDeclaration($parentNode, $modifiers);
case TokenKind::FunctionKeyword:
return $this->parseMethodDeclaration($parentNode, $modifiers);
case TokenKind::QuestionToken:
return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration(
$parentNode,
$modifiers,
$this->eat1(TokenKind::QuestionToken)
);
case TokenKind::VariableName:
return $this->parsePropertyDeclaration($parentNode, $modifiers);
case TokenKind::UseKeyword:
return $this->parseTraitUseClause($parentNode);
default:
return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers);
}
};
}
private function parseClassDeclaration($parentNode) : Node {
$classNode = new ClassDeclaration(); // TODO verify not nested
$classNode->parent = $parentNode;
$classNode->abstractOrFinalModifier = $this->eatOptional(TokenKind::AbstractKeyword, TokenKind::FinalKeyword);
$classNode->classKeyword = $this->eat1(TokenKind::ClassKeyword);
$classNode->name = $this->eat($this->nameOrReservedWordTokens); // TODO should be any
$classNode->name->kind = TokenKind::Name;
$classNode->classBaseClause = $this->parseClassBaseClause($classNode);
$classNode->classInterfaceClause = $this->parseClassInterfaceClause($classNode);
$classNode->classMembers = $this->parseClassMembers($classNode);
return $classNode;
}
private function parseClassMembers($parentNode) : Node {
$classMembers = new ClassMembersNode();
$classMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken);
$classMembers->classMemberDeclarations = $this->parseList($classMembers, ParseContext::ClassMembers);
$classMembers->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
$classMembers->parent = $parentNode;
return $classMembers;
}
private function parseFunctionDeclaration($parentNode) {
$functionNode = new FunctionDeclaration();
$this->parseFunctionType($functionNode);
$functionNode->parent = $parentNode;
return $functionNode;
}
private function parseMethodDeclaration($parentNode, $modifiers) {
$methodDeclaration = new MethodDeclaration();
$methodDeclaration->modifiers = $modifiers;
$this->parseFunctionType($methodDeclaration, true);
$methodDeclaration->parent = $parentNode;
return $methodDeclaration;
}
private function parseParameterFn() {
return function ($parentNode) {
$parameter = new Parameter();
$parameter->parent = $parentNode;
$parameter->questionToken = $this->eatOptional1(TokenKind::QuestionToken);
$typeDeclarationList = $this->tryParseParameterTypeDeclarationList($parameter);
if ($typeDeclarationList) {
$parameter->typeDeclaration = array_shift($typeDeclarationList->children);
$parameter->typeDeclaration->parent = $parameter;
if ($typeDeclarationList->children) {
$parameter->otherTypeDeclarations = $typeDeclarationList;
}
}
$parameter->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken);
// TODO add post-parse rule that prevents assignment
// TODO add post-parse rule that requires only last parameter be variadic
$parameter->dotDotDotToken = $this->eatOptional1(TokenKind::DotDotDotToken);
$parameter->variableName = $this->eat1(TokenKind::VariableName);
$parameter->equalsToken = $this->eatOptional1(TokenKind::EqualsToken);
if ($parameter->equalsToken !== null) {
// TODO add post-parse rule that checks for invalid assignments
$parameter->default = $this->parseExpression($parameter);
}
return $parameter;
};
}
/**
* @param ArrowFunctionCreationExpression|AnonymousFunctionCreationExpression|FunctionDeclaration|MethodDeclaration $parentNode a node with FunctionReturnType trait
*/
private function parseAndSetReturnTypeDeclarationList($parentNode) {
$returnTypeList = $this->parseReturnTypeDeclarationList($parentNode);
if (!$returnTypeList) {
$parentNode->returnType = new MissingToken(TokenKind::ReturnType, $this->token->fullStart);
return;
}
$returnType = array_shift($returnTypeList->children);
$parentNode->returnType = $returnType;
$returnType->parent = $parentNode;
if ($returnTypeList->children) {
$parentNode->otherReturnTypes = $returnTypeList;
}
}
/**
* Attempt to parse the return type after the `:` and optional `?` token.
*
* @return DelimitedList\QualifiedNameList|null
*/
private function parseReturnTypeDeclarationList($parentNode) {
$result = $this->parseDelimitedList(
DelimitedList\QualifiedNameList::class,
TokenKind::BarToken,
function ($token) {
return \in_array($token->kind, $this->returnTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token);
},
function ($parentNode) {
return $this->parseReturnTypeDeclaration($parentNode);
},
$parentNode,
false);
// Add a MissingToken so that this will warn about `function () : T| {}`
// TODO: Make this a reusable abstraction?
if ($result && (end($result->children)->kind ?? null) === TokenKind::BarToken) {
$result->children[] = new MissingToken(TokenKind::ReturnType, $this->token->fullStart);
}
return $result;
}
private function parseReturnTypeDeclaration($parentNode) {
return $this->eatOptional($this->returnTypeDeclarationTokens)
?? $this->parseQualifiedName($parentNode);
}
private function tryParseParameterTypeDeclaration($parentNode) {
$parameterTypeDeclaration =
$this->eatOptional($this->parameterTypeDeclarationTokens) ?? $this->parseQualifiedName($parentNode);
return $parameterTypeDeclaration;
}
/**
* @param Node $parentNode
* @return DelimitedList\QualifiedNameList|null
*/
private function tryParseParameterTypeDeclarationList($parentNode) {
$result = $this->parseDelimitedList(
DelimitedList\QualifiedNameList::class,
TokenKind::BarToken,
function ($token) {
return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token);
},
function ($parentNode) {
return $this->tryParseParameterTypeDeclaration($parentNode);
},
$parentNode,
true);
// Add a MissingToken so that this will Warn about `function (T| $x) {}`
// TODO: Make this a reusable abstraction?
if ($result && (end($result->children)->kind ?? null) === TokenKind::BarToken) {
$result->children[] = new MissingToken(TokenKind::Name, $this->token->fullStart);
}
return $result;
}
private function parseCompoundStatement($parentNode) {
$compoundStatement = new CompoundStatementNode();
$compoundStatement->openBrace = $this->eat1(TokenKind::OpenBraceToken);
$compoundStatement->statements = $this->parseList($compoundStatement, ParseContext::BlockStatements);
$compoundStatement->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
$compoundStatement->parent = $parentNode;
return $compoundStatement;
}
private function array_push_list(& $array, $list) {
foreach ($list as $item) {
$array[] = $item;
}
}
private function isClassMemberDeclarationStart(Token $token) {
switch ($token->kind) {
// const-modifier
case TokenKind::ConstKeyword:
// visibility-modifier
case TokenKind::PublicKeyword:
case TokenKind::ProtectedKeyword:
case TokenKind::PrivateKeyword:
// static-modifier
case TokenKind::StaticKeyword:
// class-modifier
case TokenKind::AbstractKeyword:
case TokenKind::FinalKeyword:
case TokenKind::VarKeyword:
case TokenKind::FunctionKeyword:
case TokenKind::UseKeyword:
return true;
}
return false;
}
private function isStatementStart(Token $token) {
// https://github.com/php/php-langspec/blob/master/spec/19-grammar.md#statements
switch ($token->kind) {
// Compound Statements
case TokenKind::OpenBraceToken:
// Labeled Statements
case TokenKind::Name:
// case TokenKind::CaseKeyword: // TODO update spec
// case TokenKind::DefaultKeyword:
// Expression Statements
case TokenKind::SemicolonToken:
case TokenKind::IfKeyword:
case TokenKind::SwitchKeyword:
// Iteration Statements
case TokenKind::WhileKeyword:
case TokenKind::DoKeyword:
case TokenKind::ForKeyword:
case TokenKind::ForeachKeyword:
// Jump Statements
case TokenKind::GotoKeyword:
case TokenKind::ContinueKeyword:
case TokenKind::BreakKeyword:
case TokenKind::ReturnKeyword:
case TokenKind::ThrowKeyword:
// The try Statement
case TokenKind::TryKeyword:
// The declare Statement
case TokenKind::DeclareKeyword:
// const-declaration
case TokenKind::ConstKeyword:
// function-definition
case TokenKind::FunctionKeyword:
// class-declaration
case TokenKind::ClassKeyword:
case TokenKind::AbstractKeyword:
case TokenKind::FinalKeyword:
// interface-declaration
case TokenKind::InterfaceKeyword:
// trait-declaration
case TokenKind::TraitKeyword:
// namespace-definition
case TokenKind::NamespaceKeyword:
// namespace-use-declaration
case TokenKind::UseKeyword:
// global-declaration
case TokenKind::GlobalKeyword:
// function-static-declaration
case TokenKind::StaticKeyword:
case TokenKind::ScriptSectionEndTag:
return true;
default:
return $this->isExpressionStart($token);
}
}
private function isExpressionStart($token) {
return ($this->isExpressionStartFn())($token);
}
private function isExpressionStartFn() {
return function ($token) {
switch ($token->kind) {
// Script Inclusion Expression
case TokenKind::RequireKeyword:
case TokenKind::RequireOnceKeyword:
case TokenKind::IncludeKeyword:
case TokenKind::IncludeOnceKeyword:
// yield-expression
case TokenKind::YieldKeyword:
case TokenKind::YieldFromKeyword:
// object-creation-expression
case TokenKind::NewKeyword:
case TokenKind::CloneKeyword:
return true;
// unary-op-expression
case TokenKind::PlusToken:
case TokenKind::MinusToken:
case TokenKind::ExclamationToken:
case TokenKind::TildeToken:
// error-control-expression
case TokenKind::AtSymbolToken:
// prefix-increment-expression
case TokenKind::PlusPlusToken:
// prefix-decrement-expression
case TokenKind::MinusMinusToken:
return true;
// variable-name
case TokenKind::VariableName:
case TokenKind::DollarToken:
return true;
// qualified-name
case TokenKind::Name:
case TokenKind::BackslashToken:
return true;
case TokenKind::NamespaceKeyword:
// TODO currently only supports qualified-names, but eventually parse namespace declarations
return $this->isNamespaceKeywordStartOfExpression($token);
// literal
case TokenKind::DecimalLiteralToken: // TODO merge dec, oct, hex, bin, float -> NumericLiteral
case TokenKind::OctalLiteralToken:
case TokenKind::HexadecimalLiteralToken:
case TokenKind::BinaryLiteralToken:
case TokenKind::FloatingLiteralToken:
case TokenKind::InvalidOctalLiteralToken:
case TokenKind::InvalidHexadecimalLiteral:
case TokenKind::InvalidBinaryLiteral:
case TokenKind::IntegerLiteralToken:
case TokenKind::StringLiteralToken:
case TokenKind::SingleQuoteToken:
case TokenKind::DoubleQuoteToken:
case TokenKind::HeredocStart:
case TokenKind::BacktickToken:
// array-creation-expression
case TokenKind::ArrayKeyword:
case TokenKind::OpenBracketToken:
// intrinsic-construct
case TokenKind::EchoKeyword:
case TokenKind::ListKeyword:
case TokenKind::UnsetKeyword:
// intrinsic-operator
case TokenKind::EmptyKeyword:
case TokenKind::EvalKeyword:
case TokenKind::ExitKeyword:
case TokenKind::DieKeyword:
case TokenKind::IsSetKeyword:
case TokenKind::PrintKeyword:
// ( expression )
case TokenKind::OpenParenToken:
case TokenKind::ArrayCastToken:
case TokenKind::BoolCastToken:
case TokenKind::DoubleCastToken:
case TokenKind::IntCastToken:
case TokenKind::ObjectCastToken:
case TokenKind::StringCastToken:
case TokenKind::UnsetCastToken:
// anonymous-function-creation-expression
case TokenKind::StaticKeyword:
case TokenKind::FunctionKeyword:
case TokenKind::FnKeyword:
return true;
}
return \in_array($token->kind, $this->reservedWordTokens, true);
};
}
/**
* Handles the fact that $token may either be getCurrentToken or the token immediately before it in isExpressionStartFn().
* An expression can be namespace\CONST, namespace\fn(), or namespace\ClassName
*/
private function isNamespaceKeywordStartOfExpression(Token $token) : bool {
$nextToken = $this->getCurrentToken();
if ($nextToken->kind === TokenKind::BackslashToken) {
return true;
}
if ($nextToken !== $token) {
return false;
}
$oldPosition = $this->lexer->getCurrentPosition();
$nextToken = $this->lexer->scanNextToken();
$this->lexer->setCurrentPosition($oldPosition);
return $nextToken->kind === TokenKind::BackslashToken;
}
/**
* @param Node $parentNode
* @return Token|MissingToken|Node
*/
private function parsePrimaryExpression($parentNode) {
$token = $this->getCurrentToken();
switch ($token->kind) {
// variable-name
case TokenKind::VariableName: // TODO special case $this
case TokenKind::DollarToken:
return $this->parseSimpleVariable($parentNode);
// qualified-name
case TokenKind::Name: // TODO Qualified name
case TokenKind::BackslashToken:
case TokenKind::NamespaceKeyword:
return $this->parseQualifiedName($parentNode);
case TokenKind::DecimalLiteralToken: // TODO merge dec, oct, hex, bin, float -> NumericLiteral
case TokenKind::OctalLiteralToken:
case TokenKind::HexadecimalLiteralToken:
case TokenKind::BinaryLiteralToken:
case TokenKind::FloatingLiteralToken:
case TokenKind::InvalidOctalLiteralToken:
case TokenKind::InvalidHexadecimalLiteral:
case TokenKind::InvalidBinaryLiteral:
case TokenKind::IntegerLiteralToken:
return $this->parseNumericLiteralExpression($parentNode);
case TokenKind::StringLiteralToken:
return $this->parseStringLiteralExpression($parentNode);
case TokenKind::DoubleQuoteToken:
case TokenKind::SingleQuoteToken:
case TokenKind::HeredocStart:
case TokenKind::BacktickToken:
return $this->parseStringLiteralExpression2($parentNode);
// TODO constant-expression
// array-creation-expression
case TokenKind::ArrayKeyword:
case TokenKind::OpenBracketToken:
return $this->parseArrayCreationExpression($parentNode);
// intrinsic-construct
case TokenKind::ListKeyword:
return $this->parseListIntrinsicExpression($parentNode);
// intrinsic-operator
case TokenKind::EmptyKeyword:
return $this->parseEmptyIntrinsicExpression($parentNode);
case TokenKind::EvalKeyword:
return $this->parseEvalIntrinsicExpression($parentNode);
case TokenKind::ExitKeyword:
case TokenKind::DieKeyword:
return $this->parseExitIntrinsicExpression($parentNode);
case TokenKind::IsSetKeyword:
return $this->parseIssetIntrinsicExpression($parentNode);
case TokenKind::PrintKeyword:
return $this->parsePrintIntrinsicExpression($parentNode);
// ( expression )
case TokenKind::OpenParenToken:
return $this->parseParenthesizedExpression($parentNode);
// anonymous-function-creation-expression
case TokenKind::StaticKeyword:
// handle `static::`, `static(`, `new static;`, `instanceof static`
if (!$this->lookahead([TokenKind::FunctionKeyword, TokenKind::FnKeyword])) {
// TODO: Should this check the parent type to reject `$x = static;`, `$x = static();`, etc.
return $this->parseStaticQualifiedName($parentNode);
}
// Could be `static function` anonymous function creation expression, so flow through
case TokenKind::FunctionKeyword:
case TokenKind::FnKeyword:
return $this->parseAnonymousFunctionCreationExpression($parentNode);
case TokenKind::TrueReservedWord:
case TokenKind::FalseReservedWord:
case TokenKind::NullReservedWord:
// handle `true::`, `true(`, `true\`
if ($this->lookahead([TokenKind::BackslashToken, TokenKind::ColonColonToken, TokenKind::OpenParenToken])) {
return $this->parseQualifiedName($parentNode);
}
return $this->parseReservedWordExpression($parentNode);
}
if (\in_array($token->kind, TokenStringMaps::RESERVED_WORDS)) {
return $this->parseQualifiedName($parentNode);
}
return new MissingToken(TokenKind::Expression, $token->fullStart);
}
private function parseEmptyStatement($parentNode) {
$emptyStatement = new EmptyStatement();
$emptyStatement->parent = $parentNode;
$emptyStatement->semicolon = $this->eat1(TokenKind::SemicolonToken);
return $emptyStatement;
}
private function parseStringLiteralExpression($parentNode) {
// TODO validate input token
$expression = new StringLiteral();
$expression->parent = $parentNode;
$expression->children = $this->getCurrentToken(); // TODO - merge string types
$this->advanceToken();
return $expression;
}
private function parseStringLiteralExpression2($parentNode) {
// TODO validate input token
$expression = new StringLiteral();
$expression->parent = $parentNode;
$expression->startQuote = $this->eat(TokenKind::SingleQuoteToken, TokenKind::DoubleQuoteToken, TokenKind::HeredocStart, TokenKind::BacktickToken);
$expression->children = array();
while (true) {
switch ($this->getCurrentToken()->kind) {
case TokenKind::DollarOpenBraceToken:
case TokenKind::OpenBraceDollarToken:
$expression->children[] = $this->eat(TokenKind::DollarOpenBraceToken, TokenKind::OpenBraceDollarToken);
// TODO: Reject ${var->prop} and ${(var->prop)} without rejecting ${var+otherVar}
// Currently, this fails to reject ${var->prop} (because `var` has TokenKind::Name instead of StringVarname)
if ($this->getCurrentToken()->kind === TokenKind::StringVarname) {
$expression->children[] = $this->parseComplexDollarTemplateStringExpression($expression);
} else {
$expression->children[] = $this->parseExpression($expression);
}
$expression->children[] = $this->eat1(TokenKind::CloseBraceToken);
break;
case $startQuoteKind = $expression->startQuote->kind:
case TokenKind::EndOfFileToken:
case TokenKind::HeredocEnd:
$expression->endQuote = $this->eat($startQuoteKind, TokenKind::HeredocEnd);
return $expression;
case TokenKind::VariableName:
$expression->children[] = $this->parseTemplateStringExpression($expression);
break;
default:
$expression->children[] = $this->getCurrentToken();
$this->advanceToken();
break;
}
}
}
/**
* This is used to parse the contents of `"${...}"` expressions.
*
* Supported: x, x[0], x[$y]
* Not supported: $x->p1, x[0][1], etc.
* @see parseTemplateStringExpression
*
* Precondition: getCurrentToken()->kind === TokenKind::StringVarname
*/
private function parseComplexDollarTemplateStringExpression($parentNode) {
$var = $this->parseSimpleVariable($parentNode);
$token = $this->getCurrentToken();
if ($token->kind === TokenKind::OpenBracketToken) {
return $this->parseTemplateStringSubscriptExpression($var);
}
return $var;
}
/**
* Double-quoted and heredoc strings support a basic set of expression types, described in http://php.net/manual/en/language.types.string.php#language.types.string.parsing
* Supported: $x, $x->p, $x[0], $x[$y]
* Not supported: $x->p1->p2, $x[0][1], etc.
* Since there is a relatively small finite set of allowed forms, I implement it here rather than trying to reuse the general expression parsing code.
*/
private function parseTemplateStringExpression($parentNode) {
$token = $this->getCurrentToken();
if ($token->kind === TokenKind::VariableName) {
$var = $this->parseSimpleVariable($parentNode);
$token = $this->getCurrentToken();
if ($token->kind === TokenKind::OpenBracketToken) {
return $this->parseTemplateStringSubscriptExpression($var);
} else if ($token->kind === TokenKind::ArrowToken) {
return $this->parseTemplateStringMemberAccessExpression($var);
} else {
return $var;
}
}
return null;
}
private function parseTemplateStringSubscriptExpression($postfixExpression) : SubscriptExpression {
$subscriptExpression = new SubscriptExpression();
$subscriptExpression->parent = $postfixExpression->parent;
$postfixExpression->parent = $subscriptExpression;
$subscriptExpression->postfixExpression = $postfixExpression;
$subscriptExpression->openBracketOrBrace = $this->eat1(TokenKind::OpenBracketToken); // Only [] syntax is supported, not {}
$token = $this->getCurrentToken();
if ($token->kind === TokenKind::VariableName) {
$subscriptExpression->accessExpression = $this->parseSimpleVariable($subscriptExpression);
} elseif ($token->kind === TokenKind::IntegerLiteralToken) {
$subscriptExpression->accessExpression = $this->parseNumericLiteralExpression($subscriptExpression);
} elseif ($token->kind === TokenKind::StringLiteralToken) {
// TODO: investigate if this should add other uncommon types of tokens for strings/numbers mentioned in parsePrimaryExpression()
$subscriptExpression->accessExpression = $this->parseStringLiteralExpression($subscriptExpression);
} elseif ($token->kind === TokenKind::Name) {
$subscriptExpression->accessExpression = $this->parseTemplateStringSubscriptStringLiteral($subscriptExpression);
} else {
$subscriptExpression->accessExpression = new MissingToken(TokenKind::Expression, $token->fullStart);
}
$subscriptExpression->closeBracketOrBrace = $this->eat1(TokenKind::CloseBracketToken);
return $subscriptExpression;
}
private function parseTemplateStringSubscriptStringLiteral($parentNode) : StringLiteral {
$expression = new StringLiteral();
$expression->parent = $parentNode;
$expression->children = $this->eat1(TokenKind::Name);
return $expression;
}
private function parseTemplateStringMemberAccessExpression($expression) : MemberAccessExpression {
$memberAccessExpression = new MemberAccessExpression();
$memberAccessExpression->parent = $expression->parent;
$expression->parent = $memberAccessExpression;
$memberAccessExpression->dereferencableExpression = $expression;
$memberAccessExpression->arrowToken = $this->eat1(TokenKind::ArrowToken);
$memberAccessExpression->memberName = $this->eat1(TokenKind::Name);
return $memberAccessExpression;
}
private function parseNumericLiteralExpression($parentNode) {
$numericLiteral = new NumericLiteral();
$numericLiteral->parent = $parentNode;
$numericLiteral->children = $this->getCurrentToken();
$this->advanceToken();
return $numericLiteral;
}
private function parseReservedWordExpression($parentNode) {
$reservedWord = new ReservedWord();
$reservedWord->parent = $parentNode;
$reservedWord->children = $this->getCurrentToken();
$this->advanceToken();
return $reservedWord;
}
private function isModifier($token) {
switch ($token->kind) {
// class-modifier
case TokenKind::AbstractKeyword:
case TokenKind::FinalKeyword:
// visibility-modifier
case TokenKind::PublicKeyword:
case TokenKind::ProtectedKeyword:
case TokenKind::PrivateKeyword:
// static-modifier
case TokenKind::StaticKeyword:
// var
case TokenKind::VarKeyword:
return true;
}
return false;
}
private function parseModifiers() {
$modifiers = array();
$token = $this->getCurrentToken();
while ($this->isModifier($token)) {
$modifiers[] = $token;
$this->advanceToken();
$token = $this->getCurrentToken();
}
return $modifiers;
}
private function isParameterStartFn() {
return function ($token) {
switch ($token->kind) {
case TokenKind::DotDotDotToken:
// qualified-name
case TokenKind::Name: // http://php.net/manual/en/language.namespaces.rules.php
case TokenKind::BackslashToken:
case TokenKind::NamespaceKeyword:
case TokenKind::AmpersandToken:
case TokenKind::VariableName:
return true;
// nullable-type
case TokenKind::QuestionToken:
return true;
}
// scalar-type
return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true);
};
}
/**
* @param string $className (name of subclass of DelimitedList)
* @param int $delimiter
* @param callable $isElementStartFn
* @param callable $parseElementFn
* @param Node $parentNode
* @param bool $allowEmptyElements
* @return DelimitedList|null instance of $className
*/
private function parseDelimitedList($className, $delimiter, $isElementStartFn, $parseElementFn, $parentNode, $allowEmptyElements = false) {
// TODO consider allowing empty delimiter to be more tolerant
$node = new $className();
$token = $this->getCurrentToken();
do {
if ($isElementStartFn($token)) {
$node->addElement($parseElementFn($node));
} elseif (!$allowEmptyElements || ($allowEmptyElements && !$this->checkToken($delimiter))) {
break;
}
$delimiterToken = $this->eatOptional($delimiter);
if ($delimiterToken !== null) {
$node->addElement($delimiterToken);
}
$token = $this->getCurrentToken();
// TODO ERROR CASE - no delimiter, but a param follows
} while ($delimiterToken !== null);
$node->parent = $parentNode;
if ($node->children === null) {
return null;
}
return $node;
}
/**
* @internal
*/
const QUALIFIED_NAME_START_TOKENS = [
TokenKind::BackslashToken,
TokenKind::NamespaceKeyword,
TokenKind::Name,
];
private function isQualifiedNameStart($token) {
return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true);
}
private function isQualifiedNameStartFn() {
return function ($token) {
return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true);
};
}
private function isQualifiedNameStartForCatchFn() {
return function ($token) {
// Unfortunately, catch(int $x) is *syntactically valid* php which `php --syntax-check` would accept.
// (tolerant-php-parser is concerned with syntax, not semantics)
return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true) ||
\in_array($token->kind, $this->reservedWordTokens, true);
};
}
/**
* @return QualifiedName
*/
private function parseStaticQualifiedName($parentNode) {
$node = new QualifiedName();
$token = $this->eat(TokenKind::StaticKeyword);
$token->kind = TokenKind::Name;
$node->parent = $parentNode;
$node->nameParts = [$token];
return $node;
}
/**
* @return QualifiedName|null - returns null for invalid qualified names such as `static\` (use parseStaticQualifiedName for that)
*/
private function parseQualifiedName($parentNode) {
return ($this->parseQualifiedNameFn())($parentNode);
}
private function parseQualifiedNameFn() {
return function ($parentNode) {
$node = new QualifiedName();
$node->parent = $parentNode;
$node->relativeSpecifier = $this->parseRelativeSpecifier($node);
if (!isset($node->relativeSpecifier)) {
$node->globalSpecifier = $this->eatOptional1(TokenKind::BackslashToken);
}
$nameParts =
$this->parseDelimitedList(
DelimitedList\QualifiedNameParts::class,
TokenKind::BackslashToken,
function ($token) {
// a\static() <- INVALID (but not checked for right now)
// new a\static() <- INVALID
// new static() <- VALID
// a\static\b <- INVALID
// a\function <- INVALID
// a\true\b <-VALID
// a\b\true <-VALID
// a\static::b <-VALID
// TODO more tests
return $this->lookahead(TokenKind::BackslashToken)
? in_array($token->kind, $this->nameOrReservedWordTokens)
: in_array($token->kind, $this->nameOrStaticOrReservedWordTokens);
},
function ($parentNode) {
$name = $this->lookahead(TokenKind::BackslashToken)
? $this->eat($this->nameOrReservedWordTokens)
: $this->eat($this->nameOrStaticOrReservedWordTokens); // TODO support keyword name
$name->kind = TokenKind::Name; // bool/true/null/static should not be treated as keywords in this case
return $name;
}, $node);
if ($nameParts === null && $node->globalSpecifier === null && $node->relativeSpecifier === null) {
return null;
}
$node->nameParts = $nameParts ? $nameParts->children : [];
return $node;
};
}
private function parseRelativeSpecifier($parentNode) {
$node = new RelativeSpecifier();
$node->parent = $parentNode;
$node->namespaceKeyword = $this->eatOptional1(TokenKind::NamespaceKeyword);
if ($node->namespaceKeyword !== null) {
$node->backslash = $this->eat1(TokenKind::BackslashToken);
}
if (isset($node->backslash)) {
return $node;
}
return null;
}
/**
* @param MethodDeclaration|FunctionDeclaration|AnonymousFunctionCreationExpression $functionDeclaration
*/
private function parseFunctionType(Node $functionDeclaration, $canBeAbstract = false, $isAnonymous = false) {
$functionDeclaration->functionKeyword = $this->eat1(TokenKind::FunctionKeyword);
$functionDeclaration->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken);
$functionDeclaration->name = $isAnonymous
? $this->eatOptional($this->nameOrKeywordOrReservedWordTokens)
: $this->eat($this->nameOrKeywordOrReservedWordTokens);
if (isset($functionDeclaration->name)) {
$functionDeclaration->name->kind = TokenKind::Name;
}
if ($isAnonymous && isset($functionDeclaration->name)) {
// Anonymous functions should not have names
$functionDeclaration->name = new SkippedToken($functionDeclaration->name); // TODO instead handle this during post-walk
}
$functionDeclaration->openParen = $this->eat1(TokenKind::OpenParenToken);
$functionDeclaration->parameters = $this->parseDelimitedList(
DelimitedList\ParameterDeclarationList::class,
TokenKind::CommaToken,
$this->isParameterStartFn(),
$this->parseParameterFn(),
$functionDeclaration);
$functionDeclaration->closeParen = $this->eat1(TokenKind::CloseParenToken);
if ($isAnonymous) {
$functionDeclaration->anonymousFunctionUseClause = $this->parseAnonymousFunctionUseClause($functionDeclaration);
}
if ($this->checkToken(TokenKind::ColonToken)) {
$functionDeclaration->colonToken = $this->eat1(TokenKind::ColonToken);
$functionDeclaration->questionToken = $this->eatOptional1(TokenKind::QuestionToken);
$this->parseAndSetReturnTypeDeclarationList($functionDeclaration);
}
if ($canBeAbstract) {
$functionDeclaration->compoundStatementOrSemicolon = $this->eatOptional1(TokenKind::SemicolonToken);
}
if (!isset($functionDeclaration->compoundStatementOrSemicolon)) {
$functionDeclaration->compoundStatementOrSemicolon = $this->parseCompoundStatement($functionDeclaration);
}
}
private function parseNamedLabelStatement($parentNode) {
$namedLabelStatement = new NamedLabelStatement();
$namedLabelStatement->parent = $parentNode;
$namedLabelStatement->name = $this->eat1(TokenKind::Name);
$namedLabelStatement->colon = $this->eat1(TokenKind::ColonToken);
$namedLabelStatement->statement = $this->parseStatement($namedLabelStatement);
return $namedLabelStatement;
}
/**
* @param int|int[] ...$expectedKinds an array of one or more kinds/sets of allowed kinds in each position
*/
private function lookahead(...$expectedKinds) : bool {
$startPos = $this->lexer->getCurrentPosition();
$startToken = $this->token;
$succeeded = true;
foreach ($expectedKinds as $kind) {
$token = $this->lexer->scanNextToken();
$currentPosition = $this->lexer->getCurrentPosition();
$endOfFilePosition = $this->lexer->getEndOfFilePosition();
if (\is_array($kind)) {
$succeeded = false;
foreach ($kind as $kindOption) {
if ($currentPosition <= $endOfFilePosition && $token->kind === $kindOption) {
$succeeded = true;
break;
}
}
} else {
if ($currentPosition > $endOfFilePosition || $token->kind !== $kind) {
$succeeded = false;
break;
}
}
}
$this->lexer->setCurrentPosition($startPos);
$this->token = $startToken;
return $succeeded;
}
private function checkToken($expectedKind) : bool {
return $this->getCurrentToken()->kind === $expectedKind;
}
private function parseIfStatement($parentNode) {
$ifStatement = new IfStatementNode();
$ifStatement->parent = $parentNode;
$ifStatement->ifKeyword = $this->eat1(TokenKind::IfKeyword);
$ifStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$ifStatement->expression = $this->parseExpression($ifStatement);
$ifStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
$curTokenKind = $this->getCurrentToken()->kind;
if ($curTokenKind === TokenKind::ColonToken) {
$ifStatement->colon = $this->eat1(TokenKind::ColonToken);
$ifStatement->statements = $this->parseList($ifStatement, ParseContext::IfClause2Elements);
} else if ($curTokenKind !== TokenKind::ScriptSectionEndTag) {
// Fix #246 : properly parse `if (false) ?\>echoed text\<?php`
$ifStatement->statements = $this->parseStatement($ifStatement);
}
$ifStatement->elseIfClauses = []; // TODO - should be some standard for empty arrays vs. null?
while ($this->checkToken(TokenKind::ElseIfKeyword)) {
$ifStatement->elseIfClauses[] = $this->parseElseIfClause($ifStatement);
}
if ($this->checkToken(TokenKind::ElseKeyword)) {
$ifStatement->elseClause = $this->parseElseClause($ifStatement);
}
$ifStatement->endifKeyword = $this->eatOptional1(TokenKind::EndIfKeyword);
if ($ifStatement->endifKeyword) {
$ifStatement->semicolon = $this->eatSemicolonOrAbortStatement();
}
return $ifStatement;
}
private function parseElseIfClause($parentNode) {
$elseIfClause = new ElseIfClauseNode();
$elseIfClause->parent = $parentNode;
$elseIfClause->elseIfKeyword = $this->eat1(TokenKind::ElseIfKeyword);
$elseIfClause->openParen = $this->eat1(TokenKind::OpenParenToken);
$elseIfClause->expression = $this->parseExpression($elseIfClause);
$elseIfClause->closeParen = $this->eat1(TokenKind::CloseParenToken);
$curTokenKind = $this->getCurrentToken()->kind;
if ($curTokenKind === TokenKind::ColonToken) {
$elseIfClause->colon = $this->eat1(TokenKind::ColonToken);
$elseIfClause->statements = $this->parseList($elseIfClause, ParseContext::IfClause2Elements);
} elseif ($curTokenKind !== TokenKind::ScriptSectionEndTag) {
$elseIfClause->statements = $this->parseStatement($elseIfClause);
}
return $elseIfClause;
}
private function parseElseClause($parentNode) {
$elseClause = new ElseClauseNode();
$elseClause->parent = $parentNode;
$elseClause->elseKeyword = $this->eat1(TokenKind::ElseKeyword);
$curTokenKind = $this->getCurrentToken()->kind;
if ($curTokenKind === TokenKind::ColonToken) {
$elseClause->colon = $this->eat1(TokenKind::ColonToken);
$elseClause->statements = $this->parseList($elseClause, ParseContext::IfClause2Elements);
} elseif ($curTokenKind !== TokenKind::ScriptSectionEndTag) {
$elseClause->statements = $this->parseStatement($elseClause);
}
return $elseClause;
}
private function parseSwitchStatement($parentNode) {
$switchStatement = new SwitchStatementNode();
$switchStatement->parent = $parentNode;
$switchStatement->switchKeyword = $this->eat1(TokenKind::SwitchKeyword);
$switchStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$switchStatement->expression = $this->parseExpression($switchStatement);
$switchStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
$switchStatement->openBrace = $this->eatOptional1(TokenKind::OpenBraceToken);
$switchStatement->colon = $this->eatOptional1(TokenKind::ColonToken);
$switchStatement->caseStatements = $this->parseList($switchStatement, ParseContext::SwitchStatementElements);
if ($switchStatement->colon !== null) {
$switchStatement->endswitch = $this->eat1(TokenKind::EndSwitchKeyword);
$switchStatement->semicolon = $this->eatSemicolonOrAbortStatement();
} else {
$switchStatement->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
}
return $switchStatement;
}
private function parseCaseOrDefaultStatement() {
return function ($parentNode) {
$caseStatement = new CaseStatementNode();
$caseStatement->parent = $parentNode;
// TODO add error checking
$caseStatement->caseKeyword = $this->eat(TokenKind::CaseKeyword, TokenKind::DefaultKeyword);
if ($caseStatement->caseKeyword->kind === TokenKind::CaseKeyword) {
$caseStatement->expression = $this->parseExpression($caseStatement);
}
$caseStatement->defaultLabelTerminator = $this->eat(TokenKind::ColonToken, TokenKind::SemicolonToken);
$caseStatement->statementList = $this->parseList($caseStatement, ParseContext::CaseStatementElements);
return $caseStatement;
};
}
private function parseWhileStatement($parentNode) {
$whileStatement = new WhileStatement();
$whileStatement->parent = $parentNode;
$whileStatement->whileToken = $this->eat1(TokenKind::WhileKeyword);
$whileStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$whileStatement->expression = $this->parseExpression($whileStatement);
$whileStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
$whileStatement->colon = $this->eatOptional1(TokenKind::ColonToken);
if ($whileStatement->colon !== null) {
$whileStatement->statements = $this->parseList($whileStatement, ParseContext::WhileStatementElements);
$whileStatement->endWhile = $this->eat1(TokenKind::EndWhileKeyword);
$whileStatement->semicolon = $this->eatSemicolonOrAbortStatement();
} elseif (!$this->checkToken(TokenKind::ScriptSectionEndTag)) {
$whileStatement->statements = $this->parseStatement($whileStatement);
}
return $whileStatement;
}
/**
* @param Node $parentNode
* @param bool $force
* @return Node|MissingToken|array - The expression, or a missing token, or (if $force) an array containing a missed and skipped token
*/
private function parseExpression($parentNode, $force = false) {
$token = $this->getCurrentToken();
if ($token->kind === TokenKind::EndOfFileToken) {
return new MissingToken(TokenKind::Expression, $token->fullStart);
}
// Equivalent to (parseExpressionFn())($parentNode)
$expression = $this->parseBinaryExpressionOrHigher(0, $parentNode);
if ($force && $expression instanceof MissingToken) {
$expression = [$expression, new SkippedToken($token)];
$this->advanceToken();
}
return $expression;
}
private function parseExpressionFn() {
return function ($parentNode) {
return $this->parseBinaryExpressionOrHigher(0, $parentNode);
};
}
/**
* @param Node $parentNode
* @return Expression
*/
private function parseUnaryExpressionOrHigher($parentNode) {
$token = $this->getCurrentToken();
switch ($token->kind) {
// unary-op-expression
case TokenKind::PlusToken:
case TokenKind::MinusToken:
case TokenKind::ExclamationToken:
case TokenKind::TildeToken:
return $this->parseUnaryOpExpression($parentNode);
// error-control-expression
case TokenKind::AtSymbolToken:
return $this->parseErrorControlExpression($parentNode);
// prefix-increment-expression
case TokenKind::PlusPlusToken:
// prefix-decrement-expression
case TokenKind::MinusMinusToken:
return $this->parsePrefixUpdateExpression($parentNode);
case TokenKind::ArrayCastToken:
case TokenKind::BoolCastToken:
case TokenKind::DoubleCastToken:
case TokenKind::IntCastToken:
case TokenKind::ObjectCastToken:
case TokenKind::StringCastToken:
case TokenKind::UnsetCastToken:
return $this->parseCastExpression($parentNode);
case TokenKind::OpenParenToken:
// TODO remove duplication
if ($this->lookahead(
[TokenKind::ArrayKeyword,
TokenKind::BinaryReservedWord,
TokenKind::BoolReservedWord,
TokenKind::BooleanReservedWord,
TokenKind::DoubleReservedWord,
TokenKind::IntReservedWord,
TokenKind::IntegerReservedWord,
TokenKind::FloatReservedWord,
TokenKind::ObjectReservedWord,
TokenKind::RealReservedWord,
TokenKind::StringReservedWord,
TokenKind::UnsetKeyword], TokenKind::CloseParenToken)) {
return $this->parseCastExpressionGranular($parentNode);
}
break;
/*
case TokenKind::BacktickToken:
return $this->parseShellCommandExpression($parentNode);
case TokenKind::OpenParenToken:
// TODO
// return $this->parseCastExpressionGranular($parentNode);
break;*/
// object-creation-expression (postfix-expression)
case TokenKind::NewKeyword:
return $this->parseObjectCreationExpression($parentNode);
// clone-expression (postfix-expression)
case TokenKind::CloneKeyword:
return $this->parseCloneExpression($parentNode);
case TokenKind::YieldKeyword:
case TokenKind::YieldFromKeyword:
return $this->parseYieldExpression($parentNode);
// include-expression
// include-once-expression
// require-expression
// require-once-expression
case TokenKind::IncludeKeyword:
case TokenKind::IncludeOnceKeyword:
case TokenKind::RequireKeyword:
case TokenKind::RequireOnceKeyword:
return $this->parseScriptInclusionExpression($parentNode);
}
$expression = $this->parsePrimaryExpression($parentNode);
return $this->parsePostfixExpressionRest($expression);
}
/**
* @param int $precedence
* @param Node $parentNode
* @return Expression
*/
private function parseBinaryExpressionOrHigher($precedence, $parentNode) {
$leftOperand = $this->parseUnaryExpressionOrHigher($parentNode);
list($prevNewPrecedence, $prevAssociativity) = self::UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY;
while (true) {
$token = $this->getCurrentToken();
list($newPrecedence, $associativity) = $this->getBinaryOperatorPrecedenceAndAssociativity($token);
// Expressions using operators w/o associativity (equality, relational, instanceof)
// cannot reference identical expression types within one of their operands.
//
// Example:
// $a < $b < $c // CASE 1: INVALID
// $a < $b === $c < $d // CASE 2: VALID
//
// In CASE 1, it is expected that we stop parsing the expression after the $b token.
if ($prevAssociativity === Associativity::None && $prevNewPrecedence === $newPrecedence) {
break;
}
// Precedence and associativity properties determine whether we recurse, and continue
// building up the current operand, or whether we pop out.
//
// Example:
// $a + $b + $c // CASE 1: additive-expression (left-associative)
// $a = $b = $c // CASE 2: equality-expression (right-associative)
//
// CASE 1:
// The additive-expression is left-associative, which means we expect the grouping to be:
// ($a + $b) + $c
//
// Because both + operators have the same precedence, and the + operator is left associative,
// we expect the second + operator NOT to be consumed because $newPrecedence > $precedence => FALSE
//
// CASE 2:
// The equality-expression is right-associative, which means we expect the grouping to be:
// $a = ($b = $c)
//
// Because both = operators have the same precedence, and the = operator is right-associative,
// we expect the second = operator to be consumed because $newPrecedence >= $precedence => TRUE
$shouldConsumeCurrentOperator =
$associativity === Associativity::Right ?
$newPrecedence >= $precedence:
$newPrecedence > $precedence;
if (!$shouldConsumeCurrentOperator) {
break;
}
// Unlike every other binary expression, exponentiation operators take precedence over unary operators.
//
// Example:
// -3**2 => -9
//
// In these cases, we strip the UnaryExpression operator, and reassign $leftOperand to
// $unaryExpression->operand.
//
// After we finish building the BinaryExpression, we rebuild the UnaryExpression so that it includes
// the original operator, and the newly constructed exponentiation-expression as the operand.
$shouldOperatorTakePrecedenceOverUnary = false;
switch ($token->kind) {
case TokenKind::AsteriskAsteriskToken:
$shouldOperatorTakePrecedenceOverUnary = $leftOperand instanceof UnaryExpression;
break;
case TokenKind::EqualsToken:
case TokenKind::AsteriskAsteriskEqualsToken:
case TokenKind::AsteriskEqualsToken:
case TokenKind::SlashEqualsToken:
case TokenKind::PercentEqualsToken:
case TokenKind::PlusEqualsToken:
case TokenKind::MinusEqualsToken:
case TokenKind::DotEqualsToken:
case TokenKind::LessThanLessThanEqualsToken:
case TokenKind::GreaterThanGreaterThanEqualsToken:
case TokenKind::AmpersandEqualsToken:
case TokenKind::CaretEqualsToken:
case TokenKind::BarEqualsToken:
case TokenKind::QuestionQuestionEqualsToken:
// Workarounds for https://github.com/Microsoft/tolerant-php-parser/issues/19#issue-201714377
// Parse `!$a = $b` as `!($a = $b)` - PHP constrains the Left Hand Side of an assignment to a variable. A unary operator (`@`, `!`, etc.) is not a variable.
// Instanceof has similar constraints for the LHS.
// So does `!$a += $b`
// TODO: Any other operators?
if ($leftOperand instanceof UnaryOpExpression) {
$shouldOperatorTakePrecedenceOverUnary = true;
}
break;
case TokenKind::InstanceOfKeyword:
// Unlike assignment, the instanceof operator doesn't have restrictions on what can go in the left hand side.
// `!` is the only unary operator with lower precedence than instanceof.
if ($leftOperand instanceof UnaryOpExpression) {
if ($leftOperand->operator->kind === TokenKind::ExclamationToken) {
$shouldOperatorTakePrecedenceOverUnary = true;
}
}
break;
case TokenKind::QuestionToken:
if ($parentNode instanceof TernaryExpression) {
// Workaround to parse "a ? b : c ? d : e" as "(a ? b : c) ? d : e"
break 2;
}
break;
}
if ($shouldOperatorTakePrecedenceOverUnary) {
$unaryExpression = $leftOperand;
$leftOperand = $unaryExpression->operand;
}
$this->advanceToken();
if ($token->kind === TokenKind::EqualsToken) {
$byRefToken = $this->eatOptional1(TokenKind::AmpersandToken);
}
$leftOperand = $token->kind === TokenKind::QuestionToken ?
$this->parseTernaryExpression($leftOperand, $token, $parentNode) :
$this->makeBinaryExpression(
$leftOperand,
$token,
$byRefToken ?? null,
$this->parseBinaryExpressionOrHigher($newPrecedence, null),
$parentNode);
// Rebuild the unary expression if we deconstructed it earlier.
if ($shouldOperatorTakePrecedenceOverUnary) {
$leftOperand->parent = $unaryExpression;
$unaryExpression->operand = $leftOperand;
$leftOperand = $unaryExpression;
}
// Hold onto these values, so we know whether we've hit duplicate non-associative operators,
// and need to terminate early.
$prevNewPrecedence = $newPrecedence;
$prevAssociativity = $associativity;
}
return $leftOperand;
}
const OPERATOR_PRECEDENCE_AND_ASSOCIATIVITY =
[
// logical-inc-OR-expression-2 (L)
TokenKind::OrKeyword => [6, Associativity::Left],
// logical-exc-OR-expression-2 (L)
TokenKind::XorKeyword=> [7, Associativity::Left],
// logical-AND-expression-2 (L)
TokenKind::AndKeyword=> [8, Associativity::Left],
// simple-assignment-expression (R)
// TODO byref-assignment-expression
TokenKind::EqualsToken => [9, Associativity::Right],
// compound-assignment-expression (R)
TokenKind::AsteriskAsteriskEqualsToken => [9, Associativity::Right],
TokenKind::AsteriskEqualsToken => [9, Associativity::Right],
TokenKind::SlashEqualsToken => [9, Associativity::Right],
TokenKind::PercentEqualsToken => [9, Associativity::Right],
TokenKind::PlusEqualsToken => [9, Associativity::Right],
TokenKind::MinusEqualsToken => [9, Associativity::Right],
TokenKind::DotEqualsToken => [9, Associativity::Right],
TokenKind::LessThanLessThanEqualsToken => [9, Associativity::Right],
TokenKind::GreaterThanGreaterThanEqualsToken => [9, Associativity::Right],
TokenKind::AmpersandEqualsToken => [9, Associativity::Right],
TokenKind::CaretEqualsToken => [9, Associativity::Right],
TokenKind::BarEqualsToken => [9, Associativity::Right],
TokenKind::QuestionQuestionEqualsToken => [9, Associativity::Right],
// TODO conditional-expression (L)
TokenKind::QuestionToken => [10, Associativity::Left],
// TokenKind::ColonToken => [9, Associativity::Left],
// TODO coalesce-expression (R)
TokenKind::QuestionQuestionToken => [9, Associativity::Right],
//logical-inc-OR-expression-1 (L)
TokenKind::BarBarToken => [12, Associativity::Left],
// logical-AND-expression-1 (L)
TokenKind::AmpersandAmpersandToken => [13, Associativity::Left],
// bitwise-inc-OR-expression (L)
TokenKind::BarToken => [14, Associativity::Left],
// bitwise-exc-OR-expression (L)
TokenKind::CaretToken => [15, Associativity::Left],
// bitwise-AND-expression (L)
TokenKind::AmpersandToken => [16, Associativity::Left],
// equality-expression (X)
TokenKind::EqualsEqualsToken => [17, Associativity::None],
TokenKind::ExclamationEqualsToken => [17, Associativity::None],
TokenKind::LessThanGreaterThanToken => [17, Associativity::None],
TokenKind::EqualsEqualsEqualsToken => [17, Associativity::None],
TokenKind::ExclamationEqualsEqualsToken => [17, Associativity::None],
TokenKind::LessThanEqualsGreaterThanToken => [17, Associativity::None],
// relational-expression (X)
TokenKind::LessThanToken => [18, Associativity::None],
TokenKind::GreaterThanToken => [18, Associativity::None],
TokenKind::LessThanEqualsToken => [18, Associativity::None],
TokenKind::GreaterThanEqualsToken => [18, Associativity::None],
// shift-expression (L)
TokenKind::LessThanLessThanToken => [19, Associativity::Left],
TokenKind::GreaterThanGreaterThanToken => [19, Associativity::Left],
// additive-expression (L)
TokenKind::PlusToken => [20, Associativity::Left],
TokenKind::MinusToken => [20, Associativity::Left],
TokenKind::DotToken =>[20, Associativity::Left],
// multiplicative-expression (L)
TokenKind::AsteriskToken => [21, Associativity::Left],
TokenKind::SlashToken => [21, Associativity::Left],
TokenKind::PercentToken => [21, Associativity::Left],
// instanceof-expression (X)
TokenKind::InstanceOfKeyword => [22, Associativity::None],
// exponentiation-expression (R)
TokenKind::AsteriskAsteriskToken => [23, Associativity::Right]
];
const UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY = [-1, -1];
private function getBinaryOperatorPrecedenceAndAssociativity($token) {
return self::OPERATOR_PRECEDENCE_AND_ASSOCIATIVITY[$token->kind] ?? self::UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY;
}
/**
* @internal Do not use outside this class, this may be changed or removed.
*/
const KNOWN_ASSIGNMENT_TOKEN_SET = [
TokenKind::AsteriskAsteriskEqualsToken => true,
TokenKind::AsteriskEqualsToken => true,
TokenKind::SlashEqualsToken => true,
TokenKind::PercentEqualsToken => true,
TokenKind::PlusEqualsToken => true,
TokenKind::MinusEqualsToken => true,
TokenKind::DotEqualsToken => true,
TokenKind::LessThanLessThanEqualsToken => true,
TokenKind::GreaterThanGreaterThanEqualsToken => true,
TokenKind::AmpersandEqualsToken => true,
TokenKind::CaretEqualsToken => true,
TokenKind::BarEqualsToken => true,
TokenKind::QuestionQuestionEqualsToken => true,
// InstanceOf has other remaining issues, but this heuristic is an improvement for many common cases such as `$x && $y = $z`
];
private function makeBinaryExpression($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) {
$assignmentExpression = $operatorToken->kind === TokenKind::EqualsToken;
if ($assignmentExpression || \array_key_exists($operatorToken->kind, self::KNOWN_ASSIGNMENT_TOKEN_SET)) {
if ($leftOperand instanceof BinaryExpression) {
if (!\array_key_exists($leftOperand->operator->kind, self::KNOWN_ASSIGNMENT_TOKEN_SET)) {
// Handle cases without parenthesis, such as $x ** $y === $z, as $x ** ($y === $z)
return $this->shiftBinaryOperands($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode);
}
} elseif ($leftOperand instanceof UnaryOpExpression || $leftOperand instanceof ErrorControlExpression) {
return $this->shiftUnaryOperands($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode);
}
}
$binaryExpression = $assignmentExpression ? new AssignmentExpression() : new BinaryExpression();
$binaryExpression->parent = $parentNode;
$leftOperand->parent = $binaryExpression;
$rightOperand->parent = $binaryExpression;
$binaryExpression->leftOperand = $leftOperand;
$binaryExpression->operator = $operatorToken;
if ($binaryExpression instanceof AssignmentExpression && isset($byRefToken)) {
$binaryExpression->byRef = $byRefToken;
}
$binaryExpression->rightOperand = $rightOperand;
return $binaryExpression;
}
/**
* @param ErrorControlExpression|UnaryOpExpression $leftOperand
*/
private function shiftUnaryOperands(UnaryExpression $leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) {
$outerUnaryOpExpression = clone($leftOperand);
$inner = $this->makeBinaryExpression(
$leftOperand->operand,
$operatorToken,
$byRefToken,
$rightOperand,
$outerUnaryOpExpression
);
// Either ErrorControlExpression or a UnaryOpExpression
$outerUnaryOpExpression->parent = $parentNode;
// TODO should this binaryExpression be wrapped in a UnaryExpression?
$outerUnaryOpExpression->operand = $inner;
return $outerUnaryOpExpression;
}
private function shiftBinaryOperands(BinaryExpression $leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) {
$inner = $this->makeBinaryExpression(
$leftOperand->rightOperand,
$operatorToken,
$byRefToken,
$rightOperand,
$parentNode
);
$outer = $this->makeBinaryExpression(
$leftOperand->leftOperand,
$leftOperand->operator,
null,
$inner,
$parentNode
);
$inner->parent = $outer;
return $outer;
}
private function parseDoStatement($parentNode) {
$doStatement = new DoStatement();
$doStatement->parent = $parentNode;
$doStatement->do = $this->eat1(TokenKind::DoKeyword);
$doStatement->statement = $this->parseStatement($doStatement);
$doStatement->whileToken = $this->eat1(TokenKind::WhileKeyword);
$doStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$doStatement->expression = $this->parseExpression($doStatement);
$doStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
$doStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $doStatement;
}
private function parseForStatement($parentNode) {
$forStatement = new ForStatement();
$forStatement->parent = $parentNode;
$forStatement->for = $this->eat1(TokenKind::ForKeyword);
$forStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$forStatement->forInitializer = $this->parseExpressionList($forStatement); // TODO spec is redundant
$forStatement->exprGroupSemicolon1 = $this->eat1(TokenKind::SemicolonToken);
$forStatement->forControl = $this->parseExpressionList($forStatement);
$forStatement->exprGroupSemicolon2 = $this->eat1(TokenKind::SemicolonToken);
$forStatement->forEndOfLoop = $this->parseExpressionList($forStatement);
$forStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
$forStatement->colon = $this->eatOptional1(TokenKind::ColonToken);
if ($forStatement->colon !== null) {
$forStatement->statements = $this->parseList($forStatement, ParseContext::ForStatementElements);
$forStatement->endFor = $this->eat1(TokenKind::EndForKeyword);
$forStatement->endForSemicolon = $this->eatSemicolonOrAbortStatement();
} elseif (!$this->checkToken(TokenKind::ScriptSectionEndTag)) {
$forStatement->statements = $this->parseStatement($forStatement);
}
return $forStatement;
}
private function parseForeachStatement($parentNode) {
$foreachStatement = new ForeachStatement();
$foreachStatement->parent = $parentNode;
$foreachStatement->foreach = $this->eat1(TokenKind::ForeachKeyword);
$foreachStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$foreachStatement->forEachCollectionName = $this->parseExpression($foreachStatement);
$foreachStatement->asKeyword = $this->eat1(TokenKind::AsKeyword);
$foreachStatement->foreachKey = $this->tryParseForeachKey($foreachStatement);
$foreachStatement->foreachValue = $this->parseForeachValue($foreachStatement);
$foreachStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
$foreachStatement->colon = $this->eatOptional1(TokenKind::ColonToken);
if ($foreachStatement->colon !== null) {
$foreachStatement->statements = $this->parseList($foreachStatement, ParseContext::ForeachStatementElements);
$foreachStatement->endForeach = $this->eat1(TokenKind::EndForEachKeyword);
$foreachStatement->endForeachSemicolon = $this->eatSemicolonOrAbortStatement();
} elseif (!$this->checkToken(TokenKind::ScriptSectionEndTag)) {
$foreachStatement->statements = $this->parseStatement($foreachStatement);
}
return $foreachStatement;
}
private function tryParseForeachKey($parentNode) {
if (!$this->isExpressionStart($this->getCurrentToken())) {
return null;
}
$startPos = $this->lexer->getCurrentPosition();
$startToken = $this->getCurrentToken();
$foreachKey = new ForeachKey();
$foreachKey->parent = $parentNode;
$foreachKey->expression = $this->parseExpression($foreachKey);
if (!$this->checkToken(TokenKind::DoubleArrowToken)) {
$this->lexer->setCurrentPosition($startPos);
$this->token = $startToken;
return null;
}
$foreachKey->arrow = $this->eat1(TokenKind::DoubleArrowToken);
return $foreachKey;
}
private function parseForeachValue($parentNode) {
$foreachValue = new ForeachValue();
$foreachValue->parent = $parentNode;
$foreachValue->ampersand = $this->eatOptional1(TokenKind::AmpersandToken);
$foreachValue->expression = $this->parseExpression($foreachValue);
return $foreachValue;
}
private function parseGotoStatement($parentNode) {
$gotoStatement = new GotoStatement();
$gotoStatement->parent = $parentNode;
$gotoStatement->goto = $this->eat1(TokenKind::GotoKeyword);
$gotoStatement->name = $this->eat1(TokenKind::Name);
$gotoStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $gotoStatement;
}
private function parseBreakOrContinueStatement($parentNode) {
// TODO should be error checking if on top level
$continueStatement = new BreakOrContinueStatement();
$continueStatement->parent = $parentNode;
$continueStatement->breakOrContinueKeyword = $this->eat(TokenKind::ContinueKeyword, TokenKind::BreakKeyword);
if ($this->isExpressionStart($this->getCurrentToken())) {
$continueStatement->breakoutLevel = $this->parseExpression($continueStatement);
}
$continueStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $continueStatement;
}
private function parseReturnStatement($parentNode) {
$returnStatement = new ReturnStatement();
$returnStatement->parent = $parentNode;
$returnStatement->returnKeyword = $this->eat1(TokenKind::ReturnKeyword);
if ($this->isExpressionStart($this->getCurrentToken())) {
$returnStatement->expression = $this->parseExpression($returnStatement);
}
$returnStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $returnStatement;
}
private function parseThrowStatement($parentNode) {
$throwStatement = new ThrowStatement();
$throwStatement->parent = $parentNode;
$throwStatement->throwKeyword = $this->eat1(TokenKind::ThrowKeyword);
// TODO error for failures to parse expressions when not optional
$throwStatement->expression = $this->parseExpression($throwStatement);
$throwStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $throwStatement;
}
private function parseTryStatement($parentNode) {
$tryStatement = new TryStatement();
$tryStatement->parent = $parentNode;
$tryStatement->tryKeyword = $this->eat1(TokenKind::TryKeyword);
$tryStatement->compoundStatement = $this->parseCompoundStatement($tryStatement); // TODO verifiy this is only compound
$tryStatement->catchClauses = array(); // TODO - should be some standard for empty arrays vs. null?
while ($this->checkToken(TokenKind::CatchKeyword)) {
$tryStatement->catchClauses[] = $this->parseCatchClause($tryStatement);
}
if ($this->checkToken(TokenKind::FinallyKeyword)) {
$tryStatement->finallyClause = $this->parseFinallyClause($tryStatement);
}
return $tryStatement;
}
private function parseCatchClause($parentNode) {
$catchClause = new CatchClause();
$catchClause->parent = $parentNode;
$catchClause->catch = $this->eat1(TokenKind::CatchKeyword);
$catchClause->openParen = $this->eat1(TokenKind::OpenParenToken);
$qualifiedNameList = $this->parseQualifiedNameCatchList($catchClause)->children ?? [];
$catchClause->qualifiedName = $qualifiedNameList[0] ?? null; // TODO generate missing token or error if null
$catchClause->otherQualifiedNameList = array_slice($qualifiedNameList, 1); // TODO: Generate error if the name list has missing tokens
$catchClause->variableName = $this->eat1(TokenKind::VariableName);
$catchClause->closeParen = $this->eat1(TokenKind::CloseParenToken);
$catchClause->compoundStatement = $this->parseCompoundStatement($catchClause);
return $catchClause;
}
private function parseFinallyClause($parentNode) {
$finallyClause = new FinallyClause();
$finallyClause->parent = $parentNode;
$finallyClause->finallyToken = $this->eat1(TokenKind::FinallyKeyword);
$finallyClause->compoundStatement = $this->parseCompoundStatement($finallyClause);
return $finallyClause;
}
private function parseDeclareStatement($parentNode) {
$declareStatement = new DeclareStatement();
$declareStatement->parent = $parentNode;
$declareStatement->declareKeyword = $this->eat1(TokenKind::DeclareKeyword);
$declareStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$declareStatement->declareDirective = $this->parseDeclareDirective($declareStatement);
$declareStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
if ($this->checkToken(TokenKind::SemicolonToken)) {
$declareStatement->semicolon = $this->eatSemicolonOrAbortStatement();
} elseif ($this->checkToken(TokenKind::ColonToken)) {
$declareStatement->colon = $this->eat1(TokenKind::ColonToken);
$declareStatement->statements = $this->parseList($declareStatement, ParseContext::DeclareStatementElements);
$declareStatement->enddeclareKeyword = $this->eat1(TokenKind::EndDeclareKeyword);
$declareStatement->semicolon = $this->eatSemicolonOrAbortStatement();
} else {
$declareStatement->statements = $this->parseStatement($declareStatement);
}
return $declareStatement;
}
private function parseDeclareDirective($parentNode) {
$declareDirective = new DeclareDirective();
$declareDirective->parent = $parentNode;
$declareDirective->name = $this->eat1(TokenKind::Name);
$declareDirective->equals = $this->eat1(TokenKind::EqualsToken);
$declareDirective->literal =
$this->eat(
TokenKind::FloatingLiteralToken,
TokenKind::IntegerLiteralToken,
TokenKind::DecimalLiteralToken,
TokenKind::OctalLiteralToken,
TokenKind::HexadecimalLiteralToken,
TokenKind::BinaryLiteralToken,
TokenKind::InvalidOctalLiteralToken,
TokenKind::InvalidHexadecimalLiteral,
TokenKind::InvalidBinaryLiteral,
TokenKind::StringLiteralToken
); // TODO simplify
return $declareDirective;
}
private function parseSimpleVariable($parentNode) {
return ($this->parseSimpleVariableFn())($parentNode);
}
private function parseSimpleVariableFn() {
return function ($parentNode) {
$token = $this->getCurrentToken();
$variable = new Variable();
$variable->parent = $parentNode;
if ($token->kind === TokenKind::DollarToken) {
$variable->dollar = $this->eat1(TokenKind::DollarToken);
$token = $this->getCurrentToken();
switch ($token->kind) {
case TokenKind::OpenBraceToken:
$variable->name = $this->parseBracedExpression($variable);
break;
case TokenKind::VariableName:
case TokenKind::StringVarname:
case TokenKind::DollarToken:
$variable->name = $this->parseSimpleVariable($variable);
break;
default:
$variable->name = new MissingToken(TokenKind::VariableName, $token->fullStart);
break;
}
} elseif ($token->kind === TokenKind::VariableName || $token->kind === TokenKind::StringVarname) {
// TODO consider splitting into dollar and name.
// StringVarname is the variable name without $, used in a template string e.g. `"${foo}"`
$variable->name = $this->eat(TokenKind::VariableName, TokenKind::StringVarname);
} else {
$variable->name = new MissingToken(TokenKind::VariableName, $token->fullStart);
}
return $variable;
};
}
private function parseYieldExpression($parentNode) {
$yieldExpression = new YieldExpression();
$yieldExpression->parent = $parentNode;
$yieldExpression->yieldOrYieldFromKeyword = $this->eat(
TokenKind::YieldFromKeyword,
TokenKind::YieldKeyword
);
if ($yieldExpression->yieldOrYieldFromKeyword->kind === TokenKind::YieldFromKeyword) {
// Don't use parseArrayElement. E.g. `yield from &$varName` or `yield from $key => $varName` are both syntax errors
$arrayElement = new ArrayElement();
$arrayElement->parent = $yieldExpression;
$arrayElement->elementValue = $this->parseExpression($arrayElement);
$yieldExpression->arrayElement = $arrayElement;
} else {
// This is always an ArrayElement for backwards compatibilitiy.
// TODO: Can this be changed to a non-ArrayElement in a future release?
if ($this->isExpressionStart($this->getCurrentToken())) {
// Both `yield expr;` and `yield;` are possible.
$yieldExpression->arrayElement = $this->parseArrayElement($yieldExpression);
} else {
$yieldExpression->arrayElement = null;
}
}
return $yieldExpression;
}
private function parseScriptInclusionExpression($parentNode) {
$scriptInclusionExpression = new ScriptInclusionExpression();
$scriptInclusionExpression->parent = $parentNode;
$scriptInclusionExpression->requireOrIncludeKeyword =
$this->eat(
TokenKind::RequireKeyword, TokenKind::RequireOnceKeyword,
TokenKind::IncludeKeyword, TokenKind::IncludeOnceKeyword
);
$scriptInclusionExpression->expression = $this->parseExpression($scriptInclusionExpression);
return $scriptInclusionExpression;
}
private function parseEchoStatement($parentNode) {
$expressionStatement = new ExpressionStatement();
// TODO: Could flatten into EchoStatement instead?
$echoExpression = new EchoExpression();
$echoExpression->parent = $expressionStatement;
$echoExpression->echoKeyword = $this->eat1(TokenKind::EchoKeyword);
$echoExpression->expressions =
$this->parseExpressionList($echoExpression);
$expressionStatement->parent = $parentNode;
$expressionStatement->expression = $echoExpression;
$expressionStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $expressionStatement;
}
private function parseUnsetStatement($parentNode) {
$expressionStatement = new ExpressionStatement();
// TODO: Could flatten into UnsetStatement instead?
$unsetExpression = $this->parseUnsetIntrinsicExpression($expressionStatement);
$expressionStatement->parent = $parentNode;
$expressionStatement->expression = $unsetExpression;
$expressionStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $expressionStatement;
}
private function parseListIntrinsicExpression($parentNode) {
$listExpression = new ListIntrinsicExpression();
$listExpression->parent = $parentNode;
$listExpression->listKeyword = $this->eat1(TokenKind::ListKeyword);
$listExpression->openParen = $this->eat1(TokenKind::OpenParenToken);
// TODO - parse loosely as ArrayElementList, and validate parse tree later
$listExpression->listElements =
$this->parseArrayElementList($listExpression, DelimitedList\ListExpressionList::class);
$listExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
return $listExpression;
}
private function isArrayElementStart($token) {
return ($this->isArrayElementStartFn())($token);
}
private function isArrayElementStartFn() {
return function ($token) {
return $token->kind === TokenKind::AmpersandToken || $token->kind === TokenKind::DotDotDotToken || $this->isExpressionStart($token);
};
}
private function parseArrayElement($parentNode) {
return ($this->parseArrayElementFn())($parentNode);
}
private function parseArrayElementFn() {
return function ($parentNode) {
$arrayElement = new ArrayElement();
$arrayElement->parent = $parentNode;
if ($this->checkToken(TokenKind::AmpersandToken)) {
$arrayElement->byRef = $this->eat1(TokenKind::AmpersandToken);
$arrayElement->elementValue = $this->parseExpression($arrayElement);
} elseif ($this->checkToken(TokenKind::DotDotDotToken)) {
$arrayElement->dotDotDot = $this->eat1(TokenKind::DotDotDotToken);
$arrayElement->elementValue = $this->parseExpression($arrayElement);
} else {
$expression = $this->parseExpression($arrayElement);
if ($this->checkToken(TokenKind::DoubleArrowToken)) {
$arrayElement->elementKey = $expression;
$arrayElement->arrowToken = $this->eat1(TokenKind::DoubleArrowToken);
$arrayElement->byRef = $this->eatOptional1(TokenKind::AmpersandToken); // TODO not okay for list expressions
$arrayElement->elementValue = $this->parseExpression($arrayElement);
} else {
$arrayElement->elementValue = $expression;
}
}
return $arrayElement;
};
}
private function parseExpressionList($parentExpression) {
return $this->parseDelimitedList(
DelimitedList\ExpressionList::class,
TokenKind::CommaToken,
$this->isExpressionStartFn(),
$this->parseExpressionFn(),
$parentExpression
);
}
private function parseUnsetIntrinsicExpression($parentNode) {
$unsetExpression = new UnsetIntrinsicExpression();
$unsetExpression->parent = $parentNode;
$unsetExpression->unsetKeyword = $this->eat1(TokenKind::UnsetKeyword);
$unsetExpression->openParen = $this->eat1(TokenKind::OpenParenToken);
$unsetExpression->expressions = $this->parseExpressionList($unsetExpression);
$unsetExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
return $unsetExpression;
}
private function parseArrayCreationExpression($parentNode) {
$arrayExpression = new ArrayCreationExpression();
$arrayExpression->parent = $parentNode;
$arrayExpression->arrayKeyword = $this->eatOptional1(TokenKind::ArrayKeyword);
$arrayExpression->openParenOrBracket = $arrayExpression->arrayKeyword !== null
? $this->eat1(TokenKind::OpenParenToken)
: $this->eat1(TokenKind::OpenBracketToken);
$arrayExpression->arrayElements = $this->parseArrayElementList($arrayExpression, DelimitedList\ArrayElementList::class);
$arrayExpression->closeParenOrBracket = $arrayExpression->arrayKeyword !== null
? $this->eat1(TokenKind::CloseParenToken)
: $this->eat1(TokenKind::CloseBracketToken);
return $arrayExpression;
}
private function parseArrayElementList($listExpression, $className) {
return $this->parseDelimitedList(
$className,
TokenKind::CommaToken,
$this->isArrayElementStartFn(),
$this->parseArrayElementFn(),
$listExpression,
true
);
}
private function parseEmptyIntrinsicExpression($parentNode) {
$emptyExpression = new EmptyIntrinsicExpression();
$emptyExpression->parent = $parentNode;
$emptyExpression->emptyKeyword = $this->eat1(TokenKind::EmptyKeyword);
$emptyExpression->openParen = $this->eat1(TokenKind::OpenParenToken);
$emptyExpression->expression = $this->parseExpression($emptyExpression);
$emptyExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
return $emptyExpression;
}
private function parseEvalIntrinsicExpression($parentNode) {
$evalExpression = new EvalIntrinsicExpression();
$evalExpression->parent = $parentNode;
$evalExpression->evalKeyword = $this->eat1(TokenKind::EvalKeyword);
$evalExpression->openParen = $this->eat1(TokenKind::OpenParenToken);
$evalExpression->expression = $this->parseExpression($evalExpression);
$evalExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
return $evalExpression;
}
private function parseParenthesizedExpression($parentNode) {
$parenthesizedExpression = new ParenthesizedExpression();
$parenthesizedExpression->parent = $parentNode;
$parenthesizedExpression->openParen = $this->eat1(TokenKind::OpenParenToken);
$parenthesizedExpression->expression = $this->parseExpression($parenthesizedExpression);
$parenthesizedExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
return $parenthesizedExpression;
}
private function parseExitIntrinsicExpression($parentNode) {
$exitExpression = new ExitIntrinsicExpression();
$exitExpression->parent = $parentNode;
$exitExpression->exitOrDieKeyword = $this->eat(TokenKind::ExitKeyword, TokenKind::DieKeyword);
$exitExpression->openParen = $this->eatOptional1(TokenKind::OpenParenToken);
if ($exitExpression->openParen !== null) {
if ($this->isExpressionStart($this->getCurrentToken())) {
$exitExpression->expression = $this->parseExpression($exitExpression);
}
$exitExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
}
return $exitExpression;
}
private function parsePrintIntrinsicExpression($parentNode) {
$printExpression = new PrintIntrinsicExpression();
$printExpression->parent = $parentNode;
$printExpression->printKeyword = $this->eat1(TokenKind::PrintKeyword);
$printExpression->expression = $this->parseExpression($printExpression);
return $printExpression;
}
private function parseIssetIntrinsicExpression($parentNode) {
$issetExpression = new IssetIntrinsicExpression();
$issetExpression->parent = $parentNode;
$issetExpression->issetKeyword = $this->eat1(TokenKind::IsSetKeyword);
$issetExpression->openParen = $this->eat1(TokenKind::OpenParenToken);
$issetExpression->expressions = $this->parseExpressionList($issetExpression);
$issetExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
return $issetExpression;
}
private function parseUnaryOpExpression($parentNode) {
$unaryOpExpression = new UnaryOpExpression();
$unaryOpExpression->parent = $parentNode;
$unaryOpExpression->operator =
$this->eat(TokenKind::PlusToken, TokenKind::MinusToken, TokenKind::ExclamationToken, TokenKind::TildeToken);
$unaryOpExpression->operand = $this->parseUnaryExpressionOrHigher($unaryOpExpression);
return $unaryOpExpression;
}
private function parseErrorControlExpression($parentNode) {
$errorControlExpression = new ErrorControlExpression();
$errorControlExpression->parent = $parentNode;
$errorControlExpression->operator = $this->eat1(TokenKind::AtSymbolToken);
$errorControlExpression->operand = $this->parseUnaryExpressionOrHigher($errorControlExpression);
return $errorControlExpression;
}
private function parsePrefixUpdateExpression($parentNode) {
$prefixUpdateExpression = new PrefixUpdateExpression();
$prefixUpdateExpression->parent = $parentNode;
$prefixUpdateExpression->incrementOrDecrementOperator = $this->eat(TokenKind::PlusPlusToken, TokenKind::MinusMinusToken);
$prefixUpdateExpression->operand = $this->parsePrimaryExpression($prefixUpdateExpression);
if (!($prefixUpdateExpression->operand instanceof MissingToken)) {
$prefixUpdateExpression->operand = $this->parsePostfixExpressionRest($prefixUpdateExpression->operand, false);
}
// TODO also check operand expression validity
return $prefixUpdateExpression;
}
private function parsePostfixExpressionRest($expression, $allowUpdateExpression = true) {
$tokenKind = $this->getCurrentToken()->kind;
// `--$a++` is invalid
if ($allowUpdateExpression &&
($tokenKind === TokenKind::PlusPlusToken ||
$tokenKind === TokenKind::MinusMinusToken)) {
return $this->parseParsePostfixUpdateExpression($expression);
}
// TODO write tons of tests
if (!($expression instanceof Variable ||
$expression instanceof ParenthesizedExpression ||
$expression instanceof QualifiedName ||
$expression instanceof CallExpression ||
$expression instanceof MemberAccessExpression ||
$expression instanceof SubscriptExpression ||
$expression instanceof ScopedPropertyAccessExpression ||
$expression instanceof StringLiteral ||
$expression instanceof ArrayCreationExpression
)) {
return $expression;
}
if ($tokenKind === TokenKind::ColonColonToken) {
$expression = $this->parseScopedPropertyAccessExpression($expression, null);
return $this->parsePostfixExpressionRest($expression);
}
$tokenKind = $this->getCurrentToken()->kind;
if ($tokenKind === TokenKind::OpenBraceToken ||
$tokenKind === TokenKind::OpenBracketToken) {
$expression = $this->parseSubscriptExpression($expression);
return $this->parsePostfixExpressionRest($expression);
}
if ($expression instanceof ArrayCreationExpression) {
// Remaining postfix expressions are invalid, so abort
return $expression;
}
if ($tokenKind === TokenKind::ArrowToken) {
$expression = $this->parseMemberAccessExpression($expression);
return $this->parsePostfixExpressionRest($expression);
}
if ($tokenKind === TokenKind::OpenParenToken && !$this->isParsingUnparenthesizedObjectCreationExpression($expression)) {
$expression = $this->parseCallExpressionRest($expression);
if (!$this->checkToken(TokenKind::OpenParenToken)) {
return $this->parsePostfixExpressionRest($expression);
}
if (
$expression instanceof ParenthesizedExpression ||
$expression instanceof CallExpression ||
$expression instanceof SubscriptExpression) {
// Continue parsing the remaining brackets for expressions
// such as `(new Foo())()`, `foo()()`, `foo()['index']()`
return $this->parsePostfixExpressionRest($expression);
}
return $expression;
}
// Reached the end of the postfix-expression, so return
return $expression;
}
private function parseMemberName($parentNode) {
$token = $this->getCurrentToken();
switch ($token->kind) {
case TokenKind::Name:
$this->advanceToken(); // TODO all names should be Nodes
return $token;
case TokenKind::VariableName:
case TokenKind::DollarToken:
return $this->parseSimpleVariable($parentNode); // TODO should be simple-variable
case TokenKind::OpenBraceToken:
return $this->parseBracedExpression($parentNode);
default:
if (\in_array($token->kind, $this->nameOrKeywordOrReservedWordTokens)) {
$this->advanceToken();
$token->kind = TokenKind::Name;
return $token;
}
}
return new MissingToken(TokenKind::MemberName, $token->fullStart);
}
private function isArgumentExpressionStartFn() {
return function ($token) {
return
$token->kind === TokenKind::DotDotDotToken ? true : $this->isExpressionStart($token);
};
}
private function parseArgumentExpressionFn() {
return function ($parentNode) {
$argumentExpression = new ArgumentExpression();
$argumentExpression->parent = $parentNode;
$argumentExpression->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken);
$argumentExpression->dotDotDotToken = $this->eatOptional1(TokenKind::DotDotDotToken);
$argumentExpression->expression = $this->parseExpression($argumentExpression);
return $argumentExpression;
};
}
private function parseCallExpressionRest($expression) {
$callExpression = new CallExpression();
$callExpression->parent = $expression->parent;
$expression->parent = $callExpression;
$callExpression->callableExpression = $expression;
$callExpression->openParen = $this->eat1(TokenKind::OpenParenToken);
$callExpression->argumentExpressionList =
$this->parseArgumentExpressionList($callExpression);
$callExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
return $callExpression;
}
private function parseParsePostfixUpdateExpression($prefixExpression) {
$postfixUpdateExpression = new PostfixUpdateExpression();
$postfixUpdateExpression->operand = $prefixExpression;
$postfixUpdateExpression->parent = $prefixExpression->parent;
$prefixExpression->parent = $postfixUpdateExpression;
$postfixUpdateExpression->incrementOrDecrementOperator =
$this->eat(TokenKind::PlusPlusToken, TokenKind::MinusMinusToken);
return $postfixUpdateExpression;
}
private function parseBracedExpression($parentNode) {
$bracedExpression = new BracedExpression();
$bracedExpression->parent = $parentNode;
$bracedExpression->openBrace = $this->eat1(TokenKind::OpenBraceToken);
$bracedExpression->expression = $this->parseExpression($bracedExpression);
$bracedExpression->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
return $bracedExpression;
}
private function parseSubscriptExpression($expression) : SubscriptExpression {
$subscriptExpression = new SubscriptExpression();
$subscriptExpression->parent = $expression->parent;
$expression->parent = $subscriptExpression;
$subscriptExpression->postfixExpression = $expression;
$subscriptExpression->openBracketOrBrace = $this->eat(TokenKind::OpenBracketToken, TokenKind::OpenBraceToken);
$subscriptExpression->accessExpression = $this->isExpressionStart($this->getCurrentToken())
? $this->parseExpression($subscriptExpression)
: null; // TODO error if used in a getter
if ($subscriptExpression->openBracketOrBrace->kind === TokenKind::OpenBraceToken) {
$subscriptExpression->closeBracketOrBrace = $this->eat1(TokenKind::CloseBraceToken);
} else {
$subscriptExpression->closeBracketOrBrace = $this->eat1(TokenKind::CloseBracketToken);
}
return $subscriptExpression;
}
private function parseMemberAccessExpression($expression):MemberAccessExpression {
$memberAccessExpression = new MemberAccessExpression();
$memberAccessExpression->parent = $expression->parent;
$expression->parent = $memberAccessExpression;
$memberAccessExpression->dereferencableExpression = $expression;
$memberAccessExpression->arrowToken = $this->eat1(TokenKind::ArrowToken);
$memberAccessExpression->memberName = $this->parseMemberName($memberAccessExpression);
return $memberAccessExpression;
}
/**
* @param Node|null $expression
* @param Node|null $fallbackParentNode (Workaround for the invalid AST `use TraitName::foo as ::x`)
*/
private function parseScopedPropertyAccessExpression($expression, $fallbackParentNode): ScopedPropertyAccessExpression {
$scopedPropertyAccessExpression = new ScopedPropertyAccessExpression();
$scopedPropertyAccessExpression->parent = $expression->parent ?? $fallbackParentNode;
if ($expression instanceof Node) {
$expression->parent = $scopedPropertyAccessExpression;
$scopedPropertyAccessExpression->scopeResolutionQualifier = $expression; // TODO ensure always a Node
}
$scopedPropertyAccessExpression->doubleColon = $this->eat1(TokenKind::ColonColonToken);
$scopedPropertyAccessExpression->memberName = $this->parseMemberName($scopedPropertyAccessExpression);
return $scopedPropertyAccessExpression;
}
public function isParsingUnparenthesizedObjectCreationExpression($expression) {
if (!$this->isParsingObjectCreationExpression) {
return false;
}
if ($expression instanceof Token) {
return true;
}
while ($expression->parent) {
$expression = $expression->parent;
if ($expression instanceof ObjectCreationExpression) {
return true;
} elseif ($expression instanceof ParenthesizedExpression) {
return false;
}
}
}
private $isParsingObjectCreationExpression = false;
private function parseObjectCreationExpression($parentNode) {
$objectCreationExpression = new ObjectCreationExpression();
$objectCreationExpression->parent = $parentNode;
$objectCreationExpression->newKeword = $this->eat1(TokenKind::NewKeyword);
// TODO - add tests for this scenario
$oldIsParsingObjectCreationExpression = $this->isParsingObjectCreationExpression;
$this->isParsingObjectCreationExpression = true;
$objectCreationExpression->classTypeDesignator =
$this->eatOptional1(TokenKind::ClassKeyword) ??
$this->parseExpression($objectCreationExpression);
$this->isParsingObjectCreationExpression = $oldIsParsingObjectCreationExpression;
$objectCreationExpression->openParen = $this->eatOptional1(TokenKind::OpenParenToken);
if ($objectCreationExpression->openParen !== null) {
$objectCreationExpression->argumentExpressionList = $this->parseArgumentExpressionList($objectCreationExpression);
$objectCreationExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
}
$objectCreationExpression->classBaseClause = $this->parseClassBaseClause($objectCreationExpression);
$objectCreationExpression->classInterfaceClause = $this->parseClassInterfaceClause($objectCreationExpression);
if ($this->getCurrentToken()->kind === TokenKind::OpenBraceToken) {
$objectCreationExpression->classMembers = $this->parseClassMembers($objectCreationExpression);
}
return $objectCreationExpression;
}
private function parseArgumentExpressionList($parentNode) {
return $this->parseDelimitedList(
DelimitedList\ArgumentExpressionList::class,
TokenKind::CommaToken,
$this->isArgumentExpressionStartFn(),
$this->parseArgumentExpressionFn(),
$parentNode
);
}
/**
* @param Node|Token $leftOperand (should only be a token for invalid ASTs)
* @param Token $questionToken
* @param Node $fallbackParentNode
*/
private function parseTernaryExpression($leftOperand, $questionToken, $fallbackParentNode):TernaryExpression {
$ternaryExpression = new TernaryExpression();
if ($leftOperand instanceof Node) {
$ternaryExpression->parent = $leftOperand->parent;
$leftOperand->parent = $ternaryExpression;
} else {
$ternaryExpression->parent = $fallbackParentNode;
}
$ternaryExpression->condition = $leftOperand;
$ternaryExpression->questionToken = $questionToken;
$ternaryExpression->ifExpression = $this->isExpressionStart($this->getCurrentToken()) ? $this->parseExpression($ternaryExpression) : null;
$ternaryExpression->colonToken = $this->eat1(TokenKind::ColonToken);
$ternaryExpression->elseExpression = $this->parseBinaryExpressionOrHigher(9, $ternaryExpression);
$leftOperand = $ternaryExpression;
return $leftOperand;
}
private function parseClassInterfaceClause($parentNode) {
$classInterfaceClause = new ClassInterfaceClause();
$classInterfaceClause->parent = $parentNode;
$classInterfaceClause->implementsKeyword = $this->eatOptional1(TokenKind::ImplementsKeyword);
if ($classInterfaceClause->implementsKeyword === null) {
return null;
}
$classInterfaceClause->interfaceNameList =
$this->parseQualifiedNameList($classInterfaceClause);
return $classInterfaceClause;
}
private function parseClassBaseClause($parentNode) {
$classBaseClause = new ClassBaseClause();
$classBaseClause->parent = $parentNode;
$classBaseClause->extendsKeyword = $this->eatOptional1(TokenKind::ExtendsKeyword);
if ($classBaseClause->extendsKeyword === null) {
return null;
}
$classBaseClause->baseClass = $this->parseQualifiedName($classBaseClause);
return $classBaseClause;
}
private function parseClassConstDeclaration($parentNode, $modifiers) {
$classConstDeclaration = new ClassConstDeclaration();
$classConstDeclaration->parent = $parentNode;
$classConstDeclaration->modifiers = $modifiers;
$classConstDeclaration->constKeyword = $this->eat1(TokenKind::ConstKeyword);
$classConstDeclaration->constElements = $this->parseConstElements($classConstDeclaration);
$classConstDeclaration->semicolon = $this->eat1(TokenKind::SemicolonToken);
return $classConstDeclaration;
}
/**
* @param Node $parentNode
* @param Token[] $modifiers
* @param Token|null $questionToken
*/
private function parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers, $questionToken = null)
{
$typeDeclarationList = $this->tryParseParameterTypeDeclarationList(null);
if ($this->getCurrentToken()->kind !== TokenKind::VariableName) {
return $this->makeMissingMemberDeclaration($parentNode, $modifiers, $questionToken, $typeDeclarationList);
}
return $this->parsePropertyDeclaration($parentNode, $modifiers, $questionToken, $typeDeclarationList);
}
/**
* @param Node $parentNode
* @param Token[] $modifiers
* @param Token|null $questionToken
* @param DelimitedList\QualifiedNameList|null $typeDeclarationList
*/
private function parsePropertyDeclaration($parentNode, $modifiers, $questionToken = null, $typeDeclarationList = null) {
$propertyDeclaration = new PropertyDeclaration();
$propertyDeclaration->parent = $parentNode;
$propertyDeclaration->modifiers = $modifiers;
$propertyDeclaration->questionToken = $questionToken;
if ($typeDeclarationList) {
/** $typeDeclarationList is a Node or a Token (e.g. IntKeyword) */
$typeDeclaration = \array_shift($typeDeclarationList->children);
$propertyDeclaration->typeDeclaration = $typeDeclaration;
if ($typeDeclaration instanceof Node) {
$typeDeclaration->parent = $propertyDeclaration;
}
if ($typeDeclarationList->children) {
$propertyDeclaration->otherTypeDeclarations = $typeDeclarationList;
$typeDeclarationList->parent = $propertyDeclaration;
}
} elseif ($questionToken) {
$propertyDeclaration->typeDeclaration = new MissingToken(TokenKind::PropertyType, $this->token->fullStart);
}
$propertyDeclaration->propertyElements = $this->parseExpressionList($propertyDeclaration);
$propertyDeclaration->semicolon = $this->eat1(TokenKind::SemicolonToken);
return $propertyDeclaration;
}
/**
* @param Node $parentNode
* @return DelimitedList\QualifiedNameList
*/
private function parseQualifiedNameList($parentNode) {
return $this->parseDelimitedList(
DelimitedList\QualifiedNameList::class,
TokenKind::CommaToken,
$this->isQualifiedNameStartFn(),
$this->parseQualifiedNameFn(),
$parentNode);
}
private function parseQualifiedNameCatchList($parentNode) {
$result = $this->parseDelimitedList(
DelimitedList\QualifiedNameList::class,
TokenKind::BarToken,
$this->isQualifiedNameStartForCatchFn(),
$this->parseQualifiedNameFn(),
$parentNode);
// Add a MissingToken so that this will Warn about `catch (T| $x) {}`
// TODO: Make this a reusable abstraction?
if ($result && (end($result->children)->kind ?? null) === TokenKind::BarToken) {
$result->children[] = new MissingToken(TokenKind::Name, $this->token->fullStart);
}
return $result;
}
private function parseInterfaceDeclaration($parentNode) {
$interfaceDeclaration = new InterfaceDeclaration(); // TODO verify not nested
$interfaceDeclaration->parent = $parentNode;
$interfaceDeclaration->interfaceKeyword = $this->eat1(TokenKind::InterfaceKeyword);
$interfaceDeclaration->name = $this->eat1(TokenKind::Name);
$interfaceDeclaration->interfaceBaseClause = $this->parseInterfaceBaseClause($interfaceDeclaration);
$interfaceDeclaration->interfaceMembers = $this->parseInterfaceMembers($interfaceDeclaration);
return $interfaceDeclaration;
}
private function parseInterfaceMembers($parentNode) : Node {
$interfaceMembers = new InterfaceMembers();
$interfaceMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken);
$interfaceMembers->interfaceMemberDeclarations = $this->parseList($interfaceMembers, ParseContext::InterfaceMembers);
$interfaceMembers->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
$interfaceMembers->parent = $parentNode;
return $interfaceMembers;
}
private function isInterfaceMemberDeclarationStart(Token $token) {
switch ($token->kind) {
// visibility-modifier
case TokenKind::PublicKeyword:
case TokenKind::ProtectedKeyword:
case TokenKind::PrivateKeyword:
// static-modifier
case TokenKind::StaticKeyword:
// class-modifier
case TokenKind::AbstractKeyword:
case TokenKind::FinalKeyword:
case TokenKind::ConstKeyword:
case TokenKind::FunctionKeyword:
return true;
}
return false;
}
private function parseInterfaceElementFn() {
return function ($parentNode) {
$modifiers = $this->parseModifiers();
$token = $this->getCurrentToken();
switch ($token->kind) {
case TokenKind::ConstKeyword:
return $this->parseClassConstDeclaration($parentNode, $modifiers);
case TokenKind::FunctionKeyword:
return $this->parseMethodDeclaration($parentNode, $modifiers);
default:
$missingInterfaceMemberDeclaration = new MissingMemberDeclaration();
$missingInterfaceMemberDeclaration->parent = $parentNode;
$missingInterfaceMemberDeclaration->modifiers = $modifiers;
return $missingInterfaceMemberDeclaration;
}
};
}
private function parseInterfaceBaseClause($parentNode) {
$interfaceBaseClause = new InterfaceBaseClause();
$interfaceBaseClause->parent = $parentNode;
$interfaceBaseClause->extendsKeyword = $this->eatOptional1(TokenKind::ExtendsKeyword);
if (isset($interfaceBaseClause->extendsKeyword)) {
$interfaceBaseClause->interfaceNameList = $this->parseQualifiedNameList($interfaceBaseClause);
} else {
return null;
}
return $interfaceBaseClause;
}
private function parseNamespaceDefinition($parentNode) {
$namespaceDefinition = new NamespaceDefinition();
$namespaceDefinition->parent = $parentNode;
$namespaceDefinition->namespaceKeyword = $this->eat1(TokenKind::NamespaceKeyword);
if (!$this->checkToken(TokenKind::NamespaceKeyword)) {
$namespaceDefinition->name = $this->parseQualifiedName($namespaceDefinition); // TODO only optional with compound statement block
}
$namespaceDefinition->compoundStatementOrSemicolon =
$this->checkToken(TokenKind::OpenBraceToken) ?
$this->parseCompoundStatement($namespaceDefinition) : $this->eatSemicolonOrAbortStatement();
return $namespaceDefinition;
}
private function parseNamespaceUseDeclaration($parentNode) {
$namespaceUseDeclaration = new NamespaceUseDeclaration();
$namespaceUseDeclaration->parent = $parentNode;
$namespaceUseDeclaration->useKeyword = $this->eat1(TokenKind::UseKeyword);
$namespaceUseDeclaration->functionOrConst = $this->eatOptional(TokenKind::FunctionKeyword, TokenKind::ConstKeyword);
$namespaceUseDeclaration->useClauses = $this->parseNamespaceUseClauseList($namespaceUseDeclaration);
$namespaceUseDeclaration->semicolon = $this->eatSemicolonOrAbortStatement();
return $namespaceUseDeclaration;
}
private function parseNamespaceUseClauseList($parentNode) {
return $this->parseDelimitedList(
DelimitedList\NamespaceUseClauseList::class,
TokenKind::CommaToken,
function ($token) {
return $this->isQualifiedNameStart($token) || $token->kind === TokenKind::FunctionKeyword || $token->kind === TokenKind::ConstKeyword;
},
function ($parentNode) {
$namespaceUseClause = new NamespaceUseClause();
$namespaceUseClause->parent = $parentNode;
$namespaceUseClause->namespaceName = $this->parseQualifiedName($namespaceUseClause);
if ($this->checkToken(TokenKind::AsKeyword)) {
$namespaceUseClause->namespaceAliasingClause = $this->parseNamespaceAliasingClause($namespaceUseClause);
}
elseif ($this->checkToken(TokenKind::OpenBraceToken)) {
$namespaceUseClause->openBrace = $this->eat1(TokenKind::OpenBraceToken);
$namespaceUseClause->groupClauses = $this->parseNamespaceUseGroupClauseList($namespaceUseClause);
$namespaceUseClause->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
}
return $namespaceUseClause;
},
$parentNode
);
}
private function parseNamespaceUseGroupClauseList($parentNode) {
return $this->parseDelimitedList(
DelimitedList\NamespaceUseGroupClauseList::class,
TokenKind::CommaToken,
function ($token) {
return $this->isQualifiedNameStart($token) || $token->kind === TokenKind::FunctionKeyword || $token->kind === TokenKind::ConstKeyword;
},
function ($parentNode) {
$namespaceUseGroupClause = new NamespaceUseGroupClause();
$namespaceUseGroupClause->parent = $parentNode;
$namespaceUseGroupClause->functionOrConst = $this->eatOptional(TokenKind::FunctionKeyword, TokenKind::ConstKeyword);
$namespaceUseGroupClause->namespaceName = $this->parseQualifiedName($namespaceUseGroupClause);
if ($this->checkToken(TokenKind::AsKeyword)) {
$namespaceUseGroupClause->namespaceAliasingClause = $this->parseNamespaceAliasingClause($namespaceUseGroupClause);
}
return $namespaceUseGroupClause;
},
$parentNode
);
}
private function parseNamespaceAliasingClause($parentNode) {
$namespaceAliasingClause = new NamespaceAliasingClause();
$namespaceAliasingClause->parent = $parentNode;
$namespaceAliasingClause->asKeyword = $this->eat1(TokenKind::AsKeyword);
$namespaceAliasingClause->name = $this->eat1(TokenKind::Name);
return $namespaceAliasingClause;
}
private function parseTraitDeclaration($parentNode) {
$traitDeclaration = new TraitDeclaration();
$traitDeclaration->parent = $parentNode;
$traitDeclaration->traitKeyword = $this->eat1(TokenKind::TraitKeyword);
$traitDeclaration->name = $this->eat1(TokenKind::Name);
$traitDeclaration->traitMembers = $this->parseTraitMembers($traitDeclaration);
return $traitDeclaration;
}
private function parseTraitMembers($parentNode) {
$traitMembers = new TraitMembers();
$traitMembers->parent = $parentNode;
$traitMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken);
$traitMembers->traitMemberDeclarations = $this->parseList($traitMembers, ParseContext::TraitMembers);
$traitMembers->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
return $traitMembers;
}
private function isTraitMemberDeclarationStart($token) {
switch ($token->kind) {
// property-declaration
case TokenKind::VariableName:
// modifiers
case TokenKind::PublicKeyword:
case TokenKind::ProtectedKeyword:
case TokenKind::PrivateKeyword:
case TokenKind::VarKeyword:
case TokenKind::StaticKeyword:
case TokenKind::AbstractKeyword:
case TokenKind::FinalKeyword:
// method-declaration
case TokenKind::FunctionKeyword:
// trait-use-clauses
case TokenKind::UseKeyword:
return true;
}
return false;
}
private function parseTraitElementFn() {
return function ($parentNode) {
$modifiers = $this->parseModifiers();
$token = $this->getCurrentToken();
switch ($token->kind) {
case TokenKind::FunctionKeyword:
return $this->parseMethodDeclaration($parentNode, $modifiers);
case TokenKind::QuestionToken:
return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration(
$parentNode,
$modifiers,
$this->eat1(TokenKind::QuestionToken)
);
case TokenKind::VariableName:
return $this->parsePropertyDeclaration($parentNode, $modifiers);
case TokenKind::UseKeyword:
return $this->parseTraitUseClause($parentNode);
default:
return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers);
}
};
}
/**
* @param Node $parentNode
* @param Token[] $modifiers
* @param Token $questionToken
* @param DelimitedList\QualifiedNameList|null $typeDeclarationList
*/
private function makeMissingMemberDeclaration($parentNode, $modifiers, $questionToken = null, $typeDeclarationList = null) {
$missingTraitMemberDeclaration = new MissingMemberDeclaration();
$missingTraitMemberDeclaration->parent = $parentNode;
$missingTraitMemberDeclaration->modifiers = $modifiers;
$missingTraitMemberDeclaration->questionToken = $questionToken;
if ($typeDeclarationList) {
$missingTraitMemberDeclaration->typeDeclaration = \array_shift($typeDeclarationList->children);
$missingTraitMemberDeclaration->typeDeclaration->parent = $missingTraitMemberDeclaration;
if ($typeDeclarationList->children) {
$missingTraitMemberDeclaration->otherTypeDeclarations = $typeDeclarationList;
$typeDeclarationList->parent = $missingTraitMemberDeclaration;
}
} elseif ($questionToken) {
$missingTraitMemberDeclaration->typeDeclaration = new MissingToken(TokenKind::PropertyType, $this->token->fullStart);
}
return $missingTraitMemberDeclaration;
}
private function parseTraitUseClause($parentNode) {
$traitUseClause = new TraitUseClause();
$traitUseClause->parent = $parentNode;
$traitUseClause->useKeyword = $this->eat1(TokenKind::UseKeyword);
$traitUseClause->traitNameList = $this->parseQualifiedNameList($traitUseClause);
$traitUseClause->semicolonOrOpenBrace = $this->eat(TokenKind::OpenBraceToken, TokenKind::SemicolonToken);
if ($traitUseClause->semicolonOrOpenBrace->kind === TokenKind::OpenBraceToken) {
$traitUseClause->traitSelectAndAliasClauses = $this->parseTraitSelectAndAliasClauseList($traitUseClause);
$traitUseClause->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
}
return $traitUseClause;
}
private function parseTraitSelectAndAliasClauseList($parentNode) {
return $this->parseDelimitedList(
DelimitedList\TraitSelectOrAliasClauseList::class,
TokenKind::SemicolonToken,
$this->isQualifiedNameStartFn(),
$this->parseTraitSelectOrAliasClauseFn(),
$parentNode
);
}
private function parseTraitSelectOrAliasClauseFn() {
return function ($parentNode) {
$traitSelectAndAliasClause = new TraitSelectOrAliasClause();
$traitSelectAndAliasClause->parent = $parentNode;
$traitSelectAndAliasClause->name = // TODO update spec
$this->parseQualifiedNameOrScopedPropertyAccessExpression($traitSelectAndAliasClause);
$traitSelectAndAliasClause->asOrInsteadOfKeyword = $this->eat(TokenKind::AsKeyword, TokenKind::InsteadOfKeyword);
$traitSelectAndAliasClause->modifiers = $this->parseModifiers(); // TODO accept all modifiers, verify later
if ($traitSelectAndAliasClause->asOrInsteadOfKeyword->kind === TokenKind::InsteadOfKeyword) {
// https://github.com/Microsoft/tolerant-php-parser/issues/190
// TODO: In the next backwards incompatible release, convert targetName to a list?
$interfaceNameList = $this->parseQualifiedNameList($traitSelectAndAliasClause)->children ?? [];
$traitSelectAndAliasClause->targetName = $interfaceNameList[0] ?? new MissingToken(TokenKind::BarToken, $this->token->fullStart);
$traitSelectAndAliasClause->remainingTargetNames = array_slice($interfaceNameList, 1);
} else {
$traitSelectAndAliasClause->targetName =
$this->parseQualifiedNameOrScopedPropertyAccessExpression($traitSelectAndAliasClause);
$traitSelectAndAliasClause->remainingTargetNames = [];
}
// TODO errors for insteadof/as
return $traitSelectAndAliasClause;
};
}
private function parseQualifiedNameOrScopedPropertyAccessExpression($parentNode) {
$qualifiedNameOrScopedProperty = $this->parseQualifiedName($parentNode);
if ($this->getCurrentToken()->kind === TokenKind::ColonColonToken) {
$qualifiedNameOrScopedProperty = $this->parseScopedPropertyAccessExpression($qualifiedNameOrScopedProperty, $parentNode);
}
return $qualifiedNameOrScopedProperty;
}
private function parseGlobalDeclaration($parentNode) {
$globalDeclaration = new GlobalDeclaration();
$globalDeclaration->parent = $parentNode;
$globalDeclaration->globalKeyword = $this->eat1(TokenKind::GlobalKeyword);
$globalDeclaration->variableNameList = $this->parseDelimitedList(
DelimitedList\VariableNameList::class,
TokenKind::CommaToken,
$this->isVariableNameStartFn(),
$this->parseSimpleVariableFn(),
$globalDeclaration
);
$globalDeclaration->semicolon = $this->eatSemicolonOrAbortStatement();
return $globalDeclaration;
}
private function parseFunctionStaticDeclaration($parentNode) {
$functionStaticDeclaration = new FunctionStaticDeclaration();
$functionStaticDeclaration->parent = $parentNode;
$functionStaticDeclaration->staticKeyword = $this->eat1(TokenKind::StaticKeyword);
$functionStaticDeclaration->staticVariableNameList = $this->parseDelimitedList(
DelimitedList\StaticVariableNameList::class,
TokenKind::CommaToken,
function ($token) {
return $token->kind === TokenKind::VariableName;
},
$this->parseStaticVariableDeclarationFn(),
$functionStaticDeclaration
);
$functionStaticDeclaration->semicolon = $this->eatSemicolonOrAbortStatement();
return $functionStaticDeclaration;
}
private function isVariableNameStartFn() {
return function ($token) {
return $token->kind === TokenKind::VariableName || $token->kind === TokenKind::DollarToken;
};
}
private function parseStaticVariableDeclarationFn() {
return function ($parentNode) {
$staticVariableDeclaration = new StaticVariableDeclaration();
$staticVariableDeclaration->parent = $parentNode;
$staticVariableDeclaration->variableName = $this->eat1(TokenKind::VariableName);
$staticVariableDeclaration->equalsToken = $this->eatOptional1(TokenKind::EqualsToken);
if ($staticVariableDeclaration->equalsToken !== null) {
// TODO add post-parse rule that checks for invalid assignments
$staticVariableDeclaration->assignment = $this->parseExpression($staticVariableDeclaration);
}
return $staticVariableDeclaration;
};
}
private function parseConstDeclaration($parentNode) {
$constDeclaration = new ConstDeclaration();
$constDeclaration->parent = $parentNode;
$constDeclaration->constKeyword = $this->eat1(TokenKind::ConstKeyword);
$constDeclaration->constElements = $this->parseConstElements($constDeclaration);
$constDeclaration->semicolon = $this->eatSemicolonOrAbortStatement();
return $constDeclaration;
}
private function parseConstElements($parentNode) {
return $this->parseDelimitedList(
DelimitedList\ConstElementList::class,
TokenKind::CommaToken,
function ($token) {
return \in_array($token->kind, $this->nameOrKeywordOrReservedWordTokens);
},
$this->parseConstElementFn(),
$parentNode
);
}
private function parseConstElementFn() {
return function ($parentNode) {
$constElement = new ConstElement();
$constElement->parent = $parentNode;
$constElement->name = $this->getCurrentToken();
$this->advanceToken();
$constElement->name->kind = TokenKind::Name; // to support keyword names
$constElement->equalsToken = $this->eat1(TokenKind::EqualsToken);
// TODO add post-parse rule that checks for invalid assignments
$constElement->assignment = $this->parseExpression($constElement);
return $constElement;
};
}
private function parseCastExpression($parentNode) {
$castExpression = new CastExpression();
$castExpression->parent = $parentNode;
$castExpression->castType = $this->eat(
TokenKind::ArrayCastToken,
TokenKind::BoolCastToken,
TokenKind::DoubleCastToken,
TokenKind::IntCastToken,
TokenKind::ObjectCastToken,
TokenKind::StringCastToken,
TokenKind::UnsetCastToken
);
$castExpression->operand = $this->parseUnaryExpressionOrHigher($castExpression);
return $castExpression;
}
private function parseCastExpressionGranular($parentNode) {
$castExpression = new CastExpression();
$castExpression->parent = $parentNode;
$castExpression->openParen = $this->eat1(TokenKind::OpenParenToken);
$castExpression->castType = $this->eat(
TokenKind::ArrayKeyword,
TokenKind::BinaryReservedWord,
TokenKind::BoolReservedWord,
TokenKind::BooleanReservedWord,
TokenKind::DoubleReservedWord,
TokenKind::IntReservedWord,
TokenKind::IntegerReservedWord,
TokenKind::FloatReservedWord,
TokenKind::ObjectReservedWord,
TokenKind::RealReservedWord,
TokenKind::StringReservedWord,
TokenKind::UnsetKeyword
);
$castExpression->closeParen = $this->eat1(TokenKind::CloseParenToken);
$castExpression->operand = $this->parseUnaryExpressionOrHigher($castExpression);
return $castExpression;
}
private function parseAnonymousFunctionCreationExpression($parentNode) {
$staticModifier = $this->eatOptional1(TokenKind::StaticKeyword);
if ($this->getCurrentToken()->kind === TokenKind::FnKeyword) {
return $this->parseArrowFunctionCreationExpression($parentNode, $staticModifier);
}
$anonymousFunctionCreationExpression = new AnonymousFunctionCreationExpression();
$anonymousFunctionCreationExpression->parent = $parentNode;
$anonymousFunctionCreationExpression->staticModifier = $staticModifier;
$this->parseFunctionType($anonymousFunctionCreationExpression, false, true);
return $anonymousFunctionCreationExpression;
}
private function parseArrowFunctionCreationExpression($parentNode, $staticModifier) : ArrowFunctionCreationExpression {
$arrowFunction = new ArrowFunctionCreationExpression();
$arrowFunction->parent = $parentNode;
$arrowFunction->staticModifier = $staticModifier;
$arrowFunction->functionKeyword = $this->eat1(TokenKind::FnKeyword);
$arrowFunction->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken);
$arrowFunction->name = $this->eatOptional($this->nameOrKeywordOrReservedWordTokens);
if (isset($arrowFunction->name)) {
// Anonymous functions should not have names.
// This is based on the code for AnonymousFunctionCreationExpression.
$arrowFunction->name->kind = TokenKind::Name;
$arrowFunction->name = new SkippedToken($arrowFunction->name); // TODO instead handle this during post-walk
}
$arrowFunction->openParen = $this->eat1(TokenKind::OpenParenToken);
$arrowFunction->parameters = $this->parseDelimitedList(
DelimitedList\ParameterDeclarationList::class,
TokenKind::CommaToken,
$this->isParameterStartFn(),
$this->parseParameterFn(),
$arrowFunction);
$arrowFunction->closeParen = $this->eat1(TokenKind::CloseParenToken);
if ($this->checkToken(TokenKind::ColonToken)) {
$arrowFunction->colonToken = $this->eat1(TokenKind::ColonToken);
$arrowFunction->questionToken = $this->eatOptional1(TokenKind::QuestionToken);
$this->parseAndSetReturnTypeDeclarationList($arrowFunction);
}
$arrowFunction->arrowToken = $this->eat1(TokenKind::DoubleArrowToken);
$arrowFunction->resultExpression = $this->parseExpression($arrowFunction);
return $arrowFunction;
}
private function parseAnonymousFunctionUseClause($parentNode) {
$anonymousFunctionUseClause = new AnonymousFunctionUseClause();
$anonymousFunctionUseClause->parent = $parentNode;
$anonymousFunctionUseClause->useKeyword = $this->eatOptional1(TokenKind::UseKeyword);
if ($anonymousFunctionUseClause->useKeyword === null) {
return null;
}
$anonymousFunctionUseClause->openParen = $this->eat1(TokenKind::OpenParenToken);
$anonymousFunctionUseClause->useVariableNameList = $this->parseDelimitedList(
DelimitedList\UseVariableNameList::class,
TokenKind::CommaToken,
function ($token) {
return $token->kind === TokenKind::AmpersandToken || $token->kind === TokenKind::VariableName;
},
function ($parentNode) {
$useVariableName = new UseVariableName();
$useVariableName->parent = $parentNode;
$useVariableName->byRef = $this->eatOptional1(TokenKind::AmpersandToken);
$useVariableName->variableName = $this->eat1(TokenKind::VariableName);
return $useVariableName;
},
$anonymousFunctionUseClause
);
$anonymousFunctionUseClause->closeParen = $this->eat1(TokenKind::CloseParenToken);
return $anonymousFunctionUseClause;
}
private function parseCloneExpression($parentNode) {
$cloneExpression = new CloneExpression();
$cloneExpression->parent = $parentNode;
$cloneExpression->cloneKeyword = $this->eat1(TokenKind::CloneKeyword);
$cloneExpression->expression = $this->parseUnaryExpressionOrHigher($cloneExpression);
return $cloneExpression;
}
private function eatSemicolonOrAbortStatement() {
if ($this->getCurrentToken()->kind !== TokenKind::ScriptSectionEndTag) {
return $this->eat1(TokenKind::SemicolonToken);
}
return null;
}
private function parseInlineHtml($parentNode) {
$inlineHtml = new InlineHtml();
$inlineHtml->parent = $parentNode;
$inlineHtml->scriptSectionEndTag = $this->eatOptional1(TokenKind::ScriptSectionEndTag);
$inlineHtml->text = $this->eatOptional1(TokenKind::InlineHtml);
$inlineHtml->scriptSectionStartTag = $this->eatOptional(TokenKind::ScriptSectionStartTag, TokenKind::ScriptSectionStartWithEchoTag);
// This is the easiest way to represent `<?= "expr", "other" `
if (($inlineHtml->scriptSectionStartTag->kind ?? null) === TokenKind::ScriptSectionStartWithEchoTag) {
$echoStatement = new ExpressionStatement();
$echoExpression = new EchoExpression();
$expressionList = $this->parseExpressionList($echoExpression) ?? (new MissingToken(TokenKind::Expression, $this->token->fullStart));
$echoExpression->expressions = $expressionList;
$echoExpression->parent = $echoStatement;
$echoStatement->expression = $echoExpression;
$echoStatement->semicolon = $this->eatSemicolonOrAbortStatement();
$echoStatement->parent = $inlineHtml;
// Deliberately leave echoKeyword as null instead of MissingToken
$inlineHtml->echoStatement = $echoStatement;
}
return $inlineHtml;
}
}
class Associativity {
const None = 0;
const Left = 1;
const Right = 2;
}