Skip to content

Support php 8.0 union types for param/property/return #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class AnonymousFunctionCreationExpression extends Expression implements Function
'colonToken',
'questionToken',
'returnType',
'otherReturnTypes',

// FunctionBody
'compoundStatementOrSemicolon'
Expand Down
1 change: 1 addition & 0 deletions src/Node/Expression/ArrowFunctionCreationExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ArrowFunctionCreationExpression extends Expression implements FunctionLike
'colonToken',
'questionToken',
'returnType',
'otherReturnTypes',

// body
'arrowToken',
Expand Down
2 changes: 2 additions & 0 deletions src/Node/FunctionReturnType.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ trait FunctionReturnType {
public $questionToken;
/** @var Token|QualifiedName */
public $returnType;
/** @var DelimitedList\QualifiedNameList|null TODO: Merge with returnType in a future backwards incompatible release */
public $otherReturnTypes;
}
1 change: 1 addition & 0 deletions src/Node/MethodDeclaration.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class MethodDeclaration extends Node implements FunctionLike {
'colonToken',
'questionToken',
'returnType',
'otherReturnTypes',

// FunctionBody
'compoundStatementOrSemicolon'
Expand Down
4 changes: 4 additions & 0 deletions src/Node/MissingMemberDeclaration.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ class MissingMemberDeclaration extends Node {
/** @var QualifiedName|Token|null */
public $typeDeclaration;

/** @var DelimitedList\QualifiedNameList|null */
public $otherTypeDeclarations;

const CHILD_NAMES = [
'modifiers',
'questionToken',
'typeDeclaration',
'otherTypeDeclarations',
];
}
6 changes: 6 additions & 0 deletions src/Node/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class Parameter extends Node {
public $questionToken;
/** @var QualifiedName|Token|null */
public $typeDeclaration;
/**
* @var DelimitedList\QualifiedNameList a list of other types, to support php 8 union types while remaining backwards compatible.
* TODO: Merge with typeDeclaration in a future backwards incompatible release.
*/
public $otherTypeDeclarations;
/** @var Token|null */
public $byRefToken;
/** @var Token|null */
Expand All @@ -28,6 +33,7 @@ class Parameter extends Node {
const CHILD_NAMES = [
'questionToken',
'typeDeclaration',
'otherTypeDeclarations',
'byRefToken',
'dotDotDotToken',
'variableName',
Expand Down
7 changes: 7 additions & 0 deletions src/Node/PropertyDeclaration.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class PropertyDeclaration extends Node {
/** @var QualifiedName|Token|null */
public $typeDeclaration;

/**
* @var DelimitedList\QualifiedNameList|null
* TODO: Unify with typeDeclaration in a future backwards incompatible release
*/
public $otherTypeDeclarations;

/** @var DelimitedList\ExpressionList */
public $propertyElements;

Expand All @@ -31,6 +37,7 @@ class PropertyDeclaration extends Node {
'modifiers',
'questionToken',
'typeDeclaration',
'otherTypeDeclarations',
'propertyElements',
'semicolon'
];
Expand Down
1 change: 1 addition & 0 deletions src/Node/Statement/FunctionDeclaration.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class FunctionDeclaration extends StatementNode implements NamespacedNameInterfa
'colonToken',
'questionToken',
'returnType',
'otherReturnTypes',

// FunctionBody
'compoundStatementOrSemicolon'
Expand Down
170 changes: 126 additions & 44 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ public function __construct() {
$this->parameterTypeDeclarationTokens =
[TokenKind::ArrayKeyword, TokenKind::CallableKeyword, TokenKind::BoolReservedWord,
TokenKind::FloatReservedWord, TokenKind::IntReservedWord, TokenKind::StringReservedWord,
TokenKind::ObjectReservedWord]; // TODO update spec
$this->returnTypeDeclarationTokens = \array_merge([TokenKind::VoidReservedWord], $this->parameterTypeDeclarationTokens);
TokenKind::ObjectReservedWord, TokenKind::NullReservedWord, TokenKind::FalseReservedWord]; // TODO update spec
$this->returnTypeDeclarationTokens = \array_merge([TokenKind::VoidReservedWord, TokenKind::NullReservedWord, TokenKind::FalseReservedWord], $this->parameterTypeDeclarationTokens);
}

/**
Expand Down Expand Up @@ -656,7 +656,14 @@ private function parseParameterFn() {
$parameter = new Parameter();
$parameter->parent = $parentNode;
$parameter->questionToken = $this->eatOptional1(TokenKind::QuestionToken);
$parameter->typeDeclaration = $this->tryParseParameterTypeDeclaration($parameter);
$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
Expand All @@ -671,13 +678,52 @@ private function parseParameterFn() {
};
}

private function parseReturnTypeDeclaration($parentNode) {
$returnTypeDeclaration =
$this->eatOptional($this->returnTypeDeclarationTokens)
?? $this->parseQualifiedName($parentNode)
?? new MissingToken(TokenKind::ReturnType, $this->getCurrentToken()->fullStart);
/**
* @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;
}
}

return $returnTypeDeclaration;
/**
* 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) {
Expand All @@ -686,6 +732,31 @@ private function tryParseParameterTypeDeclaration($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);
Expand Down Expand Up @@ -1231,7 +1302,7 @@ private function isParameterStartFn() {
}

// scalar-type
return \in_array($token->kind, $this->parameterTypeDeclarationTokens);
return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true);
};
}

Expand Down Expand Up @@ -1271,33 +1342,31 @@ private function parseDelimitedList($className, $delimiter, $isElementStartFn, $
return $node;
}

/**
* @internal
*/
const QUALIFIED_NAME_START_TOKENS = [
TokenKind::BackslashToken,
TokenKind::NamespaceKeyword,
TokenKind::Name,
];

private function isQualifiedNameStart($token) {
return ($this->isQualifiedNameStartFn())($token);
return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true);
}

private function isQualifiedNameStartFn() {
return function ($token) {
switch ($token->kind) {
case TokenKind::BackslashToken:
case TokenKind::NamespaceKeyword:
case TokenKind::Name:
return true;
}
return false;
return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true);
};
}

private function isQualifiedNameStartForCatchFn() {
return function ($token) {
switch ($token->kind) {
case TokenKind::BackslashToken:
case TokenKind::NamespaceKeyword:
case TokenKind::Name:
return true;
}
// 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, $this->reservedWordTokens, true);
return \in_array($token->kind, self::QUALIFIED_NAME_START_TOKENS, true) ||
\in_array($token->kind, $this->reservedWordTokens, true);
};
}

Expand Down Expand Up @@ -1395,7 +1464,7 @@ private function parseFunctionType(Node $functionDeclaration, $canBeAbstract = f
if ($this->checkToken(TokenKind::ColonToken)) {
$functionDeclaration->colonToken = $this->eat1(TokenKind::ColonToken);
$functionDeclaration->questionToken = $this->eatOptional1(TokenKind::QuestionToken);
$functionDeclaration->returnType = $this->parseReturnTypeDeclaration($functionDeclaration);
$this->parseAndSetReturnTypeDeclarationList($functionDeclaration);
}

if ($canBeAbstract) {
Expand Down Expand Up @@ -2842,31 +2911,38 @@ private function parseClassConstDeclaration($parentNode, $modifiers) {
*/
private function parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers, $questionToken = null)
{
$typeDeclaration = $this->tryParseParameterTypeDeclaration(null);
if ($questionToken !== null && $typeDeclaration === null) {
$typeDeclaration = new MissingToken(TokenKind::PropertyType, $this->getCurrentToken()->fullStart);
}
$typeDeclarationList = $this->tryParseParameterTypeDeclarationList(null);
if ($this->getCurrentToken()->kind !== TokenKind::VariableName) {
return $this->makeMissingMemberDeclaration($parentNode, $modifiers, $questionToken, $typeDeclaration);
return $this->makeMissingMemberDeclaration($parentNode, $modifiers, $questionToken, $typeDeclarationList);
}
return $this->parsePropertyDeclaration($parentNode, $modifiers, $questionToken, $typeDeclaration);
return $this->parsePropertyDeclaration($parentNode, $modifiers, $questionToken, $typeDeclarationList);
}

/**
* @param Node $parentNode
* @param Token[] $modifiers
* @param Token|null $questionToken
* @param QualifiedName|Token|null $typeDeclaration
* @param DelimitedList\QualifiedNameList|null $typeDeclarationList
*/
private function parsePropertyDeclaration($parentNode, $modifiers, $questionToken = null, $typeDeclaration = null) {
private function parsePropertyDeclaration($parentNode, $modifiers, $questionToken = null, $typeDeclarationList = null) {
$propertyDeclaration = new PropertyDeclaration();
$propertyDeclaration->parent = $parentNode;

$propertyDeclaration->modifiers = $modifiers;
$propertyDeclaration->questionToken = $questionToken; //
$propertyDeclaration->typeDeclaration = $typeDeclaration;
if ($typeDeclaration instanceof Node) {
$typeDeclaration->parent = $propertyDeclaration;
$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);
Expand Down Expand Up @@ -3143,16 +3219,22 @@ private function parseTraitElementFn() {
* @param Node $parentNode
* @param Token[] $modifiers
* @param Token $questionToken
* @param QualifiedName|Token|null $typeDeclaration
* @param DelimitedList\QualifiedNameList|null $typeDeclarationList
*/
private function makeMissingMemberDeclaration($parentNode, $modifiers, $questionToken = null, $typeDeclaration = null) {
private function makeMissingMemberDeclaration($parentNode, $modifiers, $questionToken = null, $typeDeclarationList = null) {
$missingTraitMemberDeclaration = new MissingMemberDeclaration();
$missingTraitMemberDeclaration->parent = $parentNode;
$missingTraitMemberDeclaration->modifiers = $modifiers;
$missingTraitMemberDeclaration->questionToken = $questionToken;
$missingTraitMemberDeclaration->typeDeclaration = $typeDeclaration;
if ($typeDeclaration instanceof Node) {
$typeDeclaration->parent = $missingTraitMemberDeclaration;
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;
}
Expand Down Expand Up @@ -3397,7 +3479,7 @@ private function parseArrowFunctionCreationExpression($parentNode, $staticModifi
if ($this->checkToken(TokenKind::ColonToken)) {
$arrowFunction->colonToken = $this->eat1(TokenKind::ColonToken);
$arrowFunction->questionToken = $this->eatOptional1(TokenKind::QuestionToken);
$arrowFunction->returnType = $this->parseReturnTypeDeclaration($arrowFunction);
$this->parseAndSetReturnTypeDeclarationList($arrowFunction);
}

$arrowFunction->arrowToken = $this->eat1(TokenKind::DoubleArrowToken);
Expand Down
1 change: 1 addition & 0 deletions tests/cases/parser/abstractMethodDeclaration1.php.tree
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"colonToken": null,
"questionToken": null,
"returnType": null,
"otherReturnTypes": null,
"compoundStatementOrSemicolon": {
"kind": "SemicolonToken",
"textLength": 1
Expand Down
1 change: 1 addition & 0 deletions tests/cases/parser/abstractMethodDeclaration2.php.tree
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"colonToken": null,
"questionToken": null,
"returnType": null,
"otherReturnTypes": null,
"compoundStatementOrSemicolon": {
"kind": "SemicolonToken",
"textLength": 1
Expand Down
1 change: 1 addition & 0 deletions tests/cases/parser/abstractMethodDeclaration3.php.tree
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"colonToken": null,
"questionToken": null,
"returnType": null,
"otherReturnTypes": null,
"compoundStatementOrSemicolon": {
"CompoundStatementNode": {
"openBrace": {
Expand Down
1 change: 1 addition & 0 deletions tests/cases/parser/abstractMethodDeclaration4.php.tree
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"colonToken": null,
"questionToken": null,
"returnType": null,
"otherReturnTypes": null,
"compoundStatementOrSemicolon": {
"CompoundStatementNode": {
"openBrace": {
Expand Down
Loading