Skip to content

Commit

Permalink
Unify the parsing of all class-like member declarations
Browse files Browse the repository at this point in the history
Using a class-like member in the wrong class-like is a compile-time
error, not a parsing error. Tolerate it and parse the current/remaining
statements and leave it to the applications to warn about using
unexpected members in classes, traits, interfaces, or enums.
(Or to tolerate them for stubs for editors/IDEs)

Related to microsoft#395
  • Loading branch information
TysonAndre committed Dec 26, 2023
1 parent 3ccba97 commit 049aa8d
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 230 deletions.
7 changes: 2 additions & 5 deletions src/ParseContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@
class ParseContext {
const SourceElements = 0;
const BlockStatements = 1;
const ClassMembers = 2;
const ClasslikeMembers = 2;
const IfClause2Elements = 3;
const SwitchStatementElements = 4;
const CaseStatementElements = 5;
const WhileStatementElements = 6;
const ForStatementElements = 7;
const ForeachStatementElements = 8;
const DeclareStatementElements = 9;
const InterfaceMembers = 10;
const TraitMembers = 11;
const Count = 12;
const EnumMembers = 13;
const Count = 10;
}
249 changes: 24 additions & 225 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,8 @@ private function isListTerminator(int $parseContext) {
case ParseContext::SourceElements:
return false;

case ParseContext::InterfaceMembers:
case ParseContext::ClassMembers:
case ParseContext::ClasslikeMembers:
case ParseContext::BlockStatements:
case ParseContext::TraitMembers:
case ParseContext::EnumMembers:
return $tokenKind === TokenKind::CloseBraceToken;
case ParseContext::SwitchStatementElements:
return $tokenKind === TokenKind::CloseBraceToken || $tokenKind === TokenKind::EndSwitchKeyword;
Expand Down Expand Up @@ -345,17 +342,8 @@ private function isValidListElement($context, Token $token) {
case ParseContext::DeclareStatementElements:
return $this->isStatementStart($token);

case ParseContext::ClassMembers:
return $this->isClassMemberDeclarationStart($token);

case ParseContext::TraitMembers:
return $this->isTraitMemberDeclarationStart($token);

case ParseContext::EnumMembers:
return $this->isEnumMemberDeclarationStart($token);

case ParseContext::InterfaceMembers:
return $this->isInterfaceMemberDeclarationStart($token);
case ParseContext::ClasslikeMembers:
return $this->isClasslikeMemberDeclarationStart($token);

case ParseContext::SwitchStatementElements:
return
Expand All @@ -376,17 +364,8 @@ private function getParseListElementFn($context) {
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::EnumMembers:
return $this->parseEnumElementFn();
case ParseContext::ClasslikeMembers:
return $this->parseClasslikeElementFn();

case ParseContext::SwitchStatementElements:
return $this->parseCaseOrDefaultStatement();
Expand Down Expand Up @@ -636,15 +615,19 @@ private function parseStatementFn() {
};
}

private function parseClassElementFn() {
private function parseClasslikeElementFn() {
return function ($parentNode) {
$modifiers = $this->parseModifiers();

$token = $this->getCurrentToken();
// Note that parsing the wrong type of class element in a classlike is a compile-time error, not a parse error.
switch ($token->kind) {
case TokenKind::ConstKeyword:
return $this->parseClassConstDeclaration($parentNode, $modifiers);

case TokenKind::CaseKeyword:
return $this->parseEnumCaseDeclaration($parentNode);

case TokenKind::FunctionKeyword:
return $this->parseMethodDeclaration($parentNode, $modifiers);

Expand Down Expand Up @@ -688,14 +671,14 @@ private function parseClassDeclaration($parentNode) : Node {
$classNode->name->kind = TokenKind::Name;
$classNode->classBaseClause = $this->parseClassBaseClause($classNode);
$classNode->classInterfaceClause = $this->parseClassInterfaceClause($classNode);
$classNode->classMembers = $this->parseClassMembers($classNode);
$classNode->classMembers = $this->parseClasslikeMembers($classNode);
return $classNode;
}

private function parseClassMembers($parentNode) : ClassMembersNode {
private function parseClasslikeMembers($parentNode) : ClassMembersNode {
$classMembers = new ClassMembersNode();
$classMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken);
$classMembers->classMemberDeclarations = $this->parseList($classMembers, ParseContext::ClassMembers);
$classMembers->classMemberDeclarations = $this->parseList($classMembers, ParseContext::ClasslikeMembers);
$classMembers->closeBrace = $this->eat1(TokenKind::CloseBraceToken);
$classMembers->parent = $parentNode;
return $classMembers;
Expand Down Expand Up @@ -741,18 +724,9 @@ private function parseAttributeExpression($parentNode) {
*/
private function parseAttributeStatement($parentNode) {
$attributeGroups = $this->parseAttributeGroups(null);
if ($parentNode instanceof ClassMembersNode) {
if ($parentNode instanceof ClassMembersNode || $parentNode instanceof TraitMembers || $parentNode instanceof EnumMembers || $parentNode instanceof InterfaceMembers) {
// Create a class element or a MissingMemberDeclaration
$statement = $this->parseClassElementFn()($parentNode);
} elseif ($parentNode instanceof TraitMembers) {
// Create a trait element or a MissingMemberDeclaration
$statement = $this->parseTraitElementFn()($parentNode);
} elseif ($parentNode instanceof EnumMembers) {
// Create a enum element or a MissingMemberDeclaration
$statement = $this->parseEnumElementFn()($parentNode);
} elseif ($parentNode instanceof InterfaceMembers) {
// Create an interface element or a MissingMemberDeclaration
$statement = $this->parseInterfaceElementFn()($parentNode);
$statement = $this->parseClasslikeElementFn()($parentNode);
} else {
// Classlikes, anonymous functions, global functions, and arrow functions can have attributes. Global constants cannot.
if (in_array($this->token->kind, [TokenKind::ClassKeyword, TokenKind::TraitKeyword, TokenKind::InterfaceKeyword, TokenKind::AbstractKeyword, TokenKind::FinalKeyword, TokenKind::FunctionKeyword, TokenKind::FnKeyword, TokenKind::EnumKeyword], true) ||
Expand Down Expand Up @@ -1045,7 +1019,7 @@ private function parseCompoundStatement($parentNode) {
return $compoundStatement;
}

private function isClassMemberDeclarationStart(Token $token) {
private function isClasslikeMemberDeclarationStart(Token $token) {
switch ($token->kind) {
// const-modifier
case TokenKind::ConstKeyword:
Expand Down Expand Up @@ -1073,6 +1047,9 @@ private function isClassMemberDeclarationStart(Token $token) {

// attributes
case TokenKind::AttributeToken:

// enum
case TokenKind::CaseKeyword:
return true;

}
Expand Down Expand Up @@ -1417,7 +1394,7 @@ private function parseStringLiteralExpression2($parentNode): StringLiteral {
case TokenKind::DollarOpenBraceToken:
case TokenKind::OpenBraceDollarToken:
$expression->children[] = $this->eat(TokenKind::DollarOpenBraceToken, TokenKind::OpenBraceDollarToken);
/**
/**
* @phpstan-ignore-next-line "Strict comparison using
* === between 403|404 and 408 will always evaluate to
* false" is wrong because those tokens were eaten above
Expand Down Expand Up @@ -3286,7 +3263,7 @@ private function parseObjectCreationExpression($parentNode) {
$objectCreationExpression->classInterfaceClause = $this->parseClassInterfaceClause($objectCreationExpression);

if ($this->getCurrentToken()->kind === TokenKind::OpenBraceToken) {
$objectCreationExpression->classMembers = $this->parseClassMembers($objectCreationExpression);
$objectCreationExpression->classMembers = $this->parseClasslikeMembers($objectCreationExpression);
}

return $objectCreationExpression;
Expand Down Expand Up @@ -3474,63 +3451,12 @@ private function parseInterfaceDeclaration($parentNode): InterfaceDeclaration {
private function parseInterfaceMembers($parentNode) : InterfaceMembers {
$interfaceMembers = new InterfaceMembers();
$interfaceMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken);
$interfaceMembers->interfaceMemberDeclarations = $this->parseList($interfaceMembers, ParseContext::InterfaceMembers);
$interfaceMembers->interfaceMemberDeclarations = $this->parseList($interfaceMembers, ParseContext::ClasslikeMembers);
$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:

// readonly-modifier
case TokenKind::ReadonlyKeyword:

// class-modifier
case TokenKind::AbstractKeyword:
case TokenKind::FinalKeyword:

case TokenKind::ConstKeyword:

case TokenKind::FunctionKeyword:

case TokenKind::AttributeToken:
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);

case TokenKind::AttributeToken:
return $this->parseAttributeStatement($parentNode);

default:
$missingInterfaceMemberDeclaration = new MissingMemberDeclaration();
$missingInterfaceMemberDeclaration->parent = $parentNode;
$missingInterfaceMemberDeclaration->modifiers = $modifiers;
return $missingInterfaceMemberDeclaration;
}
};
}

private function parseInterfaceBaseClause($parentNode) {
$interfaceBaseClause = new InterfaceBaseClause();
$interfaceBaseClause->parent = $parentNode;
Expand Down Expand Up @@ -3657,75 +3583,13 @@ private function parseTraitMembers($parentNode) {

$traitMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken);

$traitMembers->traitMemberDeclarations = $this->parseList($traitMembers, ParseContext::TraitMembers);
$traitMembers->traitMemberDeclarations = $this->parseList($traitMembers, ParseContext::ClasslikeMembers);

$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:
case TokenKind::ReadonlyKeyword:
case TokenKind::ConstKeyword:

// method-declaration
case TokenKind::FunctionKeyword:

// trait-use-clauses
case TokenKind::UseKeyword:

// attributes
case TokenKind::AttributeToken:
return true;
}
return false;
}

private function parseTraitElementFn() {
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);

case TokenKind::AttributeToken:
return $this->parseAttributeStatement($parentNode);

default:
return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers);
}
};
}

private function parseEnumDeclaration($parentNode) {
$enumDeclaration = new EnumDeclaration();
$enumDeclaration->parent = $parentNode;
Expand Down Expand Up @@ -3765,78 +3629,13 @@ private function parseEnumMembers($parentNode) {

$enumMembers->openBrace = $this->eat1(TokenKind::OpenBraceToken);

$enumMembers->enumMemberDeclarations = $this->parseList($enumMembers, ParseContext::EnumMembers);
$enumMembers->enumMemberDeclarations = $this->parseList($enumMembers, ParseContext::ClasslikeMembers);

$enumMembers->closeBrace = $this->eat1(TokenKind::CloseBraceToken);

return $enumMembers;
}

private function isEnumMemberDeclarationStart($token) {
switch ($token->kind) {
// modifiers
case TokenKind::PublicKeyword:
case TokenKind::ProtectedKeyword:
case TokenKind::PrivateKeyword:
case TokenKind::StaticKeyword:
case TokenKind::AbstractKeyword:
case TokenKind::FinalKeyword:

// method-declaration
case TokenKind::FunctionKeyword:

// trait-use-clauses (enums can use traits)
case TokenKind::UseKeyword:

// cases and constants
case TokenKind::CaseKeyword:
case TokenKind::ConstKeyword:

// attributes
case TokenKind::AttributeToken:
return true;
}
return false;
}

private function parseEnumElementFn() {
return function ($parentNode) {
$modifiers = $this->parseModifiers();

$token = $this->getCurrentToken();
switch ($token->kind) {
// TODO: CaseKeyword
case TokenKind::CaseKeyword:
return $this->parseEnumCaseDeclaration($parentNode);

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);

case TokenKind::AttributeToken:
return $this->parseAttributeStatement($parentNode);

default:
return $this->parseRemainingPropertyDeclarationOrMissingMemberDeclaration($parentNode, $modifiers);
}
};
}


/**
* @param Node $parentNode
* @param Token[] $modifiers
Expand Down
5 changes: 5 additions & 0 deletions tests/cases/parser81/enums8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
class X {
// Cases are only allowed in enums. But this is a compile-time error, not a parse error
case F = 1;
}
1 change: 1 addition & 0 deletions tests/cases/parser81/enums8.php.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Loading

0 comments on commit 049aa8d

Please sign in to comment.