From 59b37a8d259496e1a3ad76381d809c505150c127 Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Sat, 12 May 2018 11:45:42 -0700 Subject: [PATCH 1/2] Fixes #103: Support parsing multiple exception types Add a new array `otherQualifiedNameList` alongside qualifiedName to `CatchClause`. (Contains remaining `Token`s and `QualifiedName`s) (This design breaks backwards compatibility as little as possible) - A future backwards incompatible release should merge the two properties into `qualifiedNameList`, or something along those lines. Add a new helper method to parse the catch list. - The new helper is required because `tests/cases/php-langspec/exception_handling/odds_and_ends.php` should not fail because `catch (int $e)` is **syntactically** valid php (in 7.x), i.e. `php --syntax-check` doesn't reject it. --- src/Node/CatchClause.php | 9 ++ src/Parser.php | 36 +++++- tests/ParserGrammarTest.php | 2 +- tests/cases/parser/tryStatement1.php.tree | 1 + tests/cases/parser/tryStatement10.php.tree | 1 + tests/cases/parser/tryStatement11.php | 5 + tests/cases/parser/tryStatement11.php.diag | 8 ++ tests/cases/parser/tryStatement11.php.tree | 100 +++++++++++++++++ tests/cases/parser/tryStatement12.php | 5 + tests/cases/parser/tryStatement12.php.diag | 1 + tests/cases/parser/tryStatement12.php.tree | 115 +++++++++++++++++++ tests/cases/parser/tryStatement13.php.diag | 44 ++++++++ tests/cases/parser/tryStatement13.php.tree | 125 +++++++++++++++++++++ tests/cases/parser/tryStatement4.php.tree | 1 + tests/cases/parser/tryStatement5.php.tree | 1 + tests/cases/parser/tryStatement6.php.tree | 1 + tests/cases/parser/tryStatement8.php.tree | 1 + tests/cases/parser/tryStatement9.php.tree | 1 + 18 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 tests/cases/parser/tryStatement11.php create mode 100644 tests/cases/parser/tryStatement11.php.diag create mode 100644 tests/cases/parser/tryStatement11.php.tree create mode 100644 tests/cases/parser/tryStatement12.php create mode 100644 tests/cases/parser/tryStatement12.php.diag create mode 100644 tests/cases/parser/tryStatement12.php.tree create mode 100644 tests/cases/parser/tryStatement13.php.diag create mode 100644 tests/cases/parser/tryStatement13.php.tree diff --git a/src/Node/CatchClause.php b/src/Node/CatchClause.php index c8cf66b..ac71b64 100644 --- a/src/Node/CatchClause.php +++ b/src/Node/CatchClause.php @@ -16,6 +16,14 @@ class CatchClause extends Node { public $openParen; /** @var QualifiedName */ public $qualifiedName; + /** + * @var QualifiedName[]|Token[] Remaining tokens and qualified names in the catch clause + * (e.g. `catch (FirstException|SecondException $x)` would contain + * the representation of `|SecondException`) + * + * TODO: In the next backwards incompatible release, replace qualifiedName with qualifiedNameList? + */ + public $otherQualifiedNameList; /** @var Token */ public $variableName; /** @var Token */ @@ -27,6 +35,7 @@ class CatchClause extends Node { 'catch', 'openParen', 'qualifiedName', + 'otherQualifiedNameList', 'variableName', 'closeParen', 'compoundStatement' diff --git a/src/Parser.php b/src/Parser.php index db1a6f4..7dff51b 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -881,7 +881,7 @@ class Parser { case TokenKind::FunctionKeyword: return true; } - return \in_array($token->kind, $this->reservedWordTokens); + return \in_array($token->kind, $this->reservedWordTokens, true); }; } @@ -1250,6 +1250,20 @@ class Parser { }; } + 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); + }; + } + private function parseQualifiedName($parentNode) { return ($this->parseQualifiedNameFn())($parentNode); } @@ -2017,7 +2031,9 @@ class Parser { $catchClause->parent = $parentNode; $catchClause->catch = $this->eat1(TokenKind::CatchKeyword); $catchClause->openParen = $this->eat1(TokenKind::OpenParenToken); - $catchClause->qualifiedName = $this->parseQualifiedName($catchClause); // TODO generate missing token or error if null + $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); @@ -2678,6 +2694,22 @@ class Parser { $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; diff --git a/tests/ParserGrammarTest.php b/tests/ParserGrammarTest.php index 5351bf4..3cec8da 100644 --- a/tests/ParserGrammarTest.php +++ b/tests/ParserGrammarTest.php @@ -100,7 +100,7 @@ class ParserGrammarTest extends TestCase { $tokens = str_replace("\r\n", "\n", json_encode($sourceFile, JSON_PRETTY_PRINT)); file_put_contents($expectedTreeFile, $tokens); - $this->assertCount(0, DiagnosticsProvider::getDiagnostics($sourceFile)); + $this->assertSame([], DiagnosticsProvider::getDiagnostics($sourceFile)); } public function outTreeProvider() { diff --git a/tests/cases/parser/tryStatement1.php.tree b/tests/cases/parser/tryStatement1.php.tree index fc38e3a..1aaceb3 100644 --- a/tests/cases/parser/tryStatement1.php.tree +++ b/tests/cases/parser/tryStatement1.php.tree @@ -62,6 +62,7 @@ ] } }, + "otherQualifiedNameList": [], "variableName": { "kind": "VariableName", "textLength": 2 diff --git a/tests/cases/parser/tryStatement10.php.tree b/tests/cases/parser/tryStatement10.php.tree index ea96c69..420bd99 100644 --- a/tests/cases/parser/tryStatement10.php.tree +++ b/tests/cases/parser/tryStatement10.php.tree @@ -42,6 +42,7 @@ "textLength": 1 }, "qualifiedName": null, + "otherQualifiedNameList": [], "variableName": { "error": "MissingToken", "kind": "VariableName", diff --git a/tests/cases/parser/tryStatement11.php b/tests/cases/parser/tryStatement11.php new file mode 100644 index 0000000..2b54e6a --- /dev/null +++ b/tests/cases/parser/tryStatement11.php @@ -0,0 +1,5 @@ + Date: Sat, 12 May 2018 13:28:59 -0700 Subject: [PATCH 2/2] Add a missing file --- tests/cases/parser/tryStatement13.php | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/cases/parser/tryStatement13.php diff --git a/tests/cases/parser/tryStatement13.php b/tests/cases/parser/tryStatement13.php new file mode 100644 index 0000000..03e811c --- /dev/null +++ b/tests/cases/parser/tryStatement13.php @@ -0,0 +1,8 @@ +