Skip to content

Commit

Permalink
[PHP 8.0] Add match expressions (#672)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba authored Jul 15, 2020
1 parent 6ec527b commit 69c5d48
Show file tree
Hide file tree
Showing 18 changed files with 2,581 additions and 2,048 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
Version 4.6.1-dev
-----------------

Nothing yet.
### Added

* [PHP 8.0] Added support for match expressions. These are represented using a new `Expr\Match_`
containing `MatchArm`s.

Version 4.6.0 (2020-07-02)
--------------------------
Expand Down
1 change: 1 addition & 0 deletions grammar/php5.y
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ reserved_non_modifiers:
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER | T_FN
| T_MATCH
;

semi_reserved:
Expand Down
29 changes: 25 additions & 4 deletions grammar/php7.y
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ reserved_non_modifiers:
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER | T_FN
| T_MATCH
;

semi_reserved:
Expand Down Expand Up @@ -399,6 +400,25 @@ case_separator:
| ';'
;

match:
T_MATCH '(' expr ')' '{' match_arm_list '}' { $$ = Expr\Match_[$3, $6]; }
;

match_arm_list:
/* empty */ { $$ = []; }
| non_empty_match_arm_list optional_comma { $$ = $1; }
;

non_empty_match_arm_list:
match_arm { init($1); }
| non_empty_match_arm_list ',' match_arm { push($1, $3); }
;

match_arm:
expr_list_allow_comma T_DOUBLE_ARROW expr { $$ = Node\MatchArm[$1, $3]; }
| T_DEFAULT optional_comma T_DOUBLE_ARROW expr { $$ = Node\MatchArm[null, $4]; }
;

while_statement:
statement { $$ = toArray($1); }
| ':' inner_statement_list T_ENDWHILE ';' { $$ = $2; }
Expand Down Expand Up @@ -666,6 +686,7 @@ expr:
| variable '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' '&' variable { $$ = Expr\AssignRef[$1, $4]; }
| new_expr { $$ = $1; }
| match { $$ = $1; }
| T_CLONE expr { $$ = Expr\Clone_[$2]; }
| variable T_PLUS_EQUAL expr { $$ = Expr\AssignOp\Plus [$1, $3]; }
| variable T_MINUS_EQUAL expr { $$ = Expr\AssignOp\Minus [$1, $3]; }
Expand Down Expand Up @@ -967,14 +988,14 @@ new_variable:

member_name:
identifier_ex { $$ = $1; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
;

property_name:
identifier { $$ = $1; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| error { $$ = Expr\Error[]; $this->errorState = 2; }
;

Expand Down
1 change: 1 addition & 0 deletions grammar/tokens.y
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
%token T_ENDDECLARE
%token T_AS
%token T_SWITCH
%token T_MATCH
%token T_ENDSWITCH
%token T_CASE
%token T_DEFAULT
Expand Down
5 changes: 5 additions & 0 deletions lib/PhpParser/Lexer/Emulative.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpParser\Lexer;
use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
use PhpParser\Lexer\TokenEmulator\TokenEmulatorInterface;
use PhpParser\Parser\Tokens;
Expand All @@ -15,9 +16,11 @@ class Emulative extends Lexer
{
const PHP_7_3 = '7.3.0dev';
const PHP_7_4 = '7.4.0dev';
const PHP_8_0 = '8.0.0dev';

const T_COALESCE_EQUAL = 1007;
const T_FN = 1008;
const T_MATCH = 1009;

const FLEXIBLE_DOC_STRING_REGEX = <<<'REGEX'
/<<<[ \t]*(['"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\1\r?\n
Expand All @@ -39,11 +42,13 @@ public function __construct(array $options = [])
parent::__construct($options);

$this->tokenEmulators[] = new FnTokenEmulator();
$this->tokenEmulators[] = new MatchTokenEmulator();
$this->tokenEmulators[] = new CoaleseEqualTokenEmulator();
$this->tokenEmulators[] = new NumericLiteralSeparatorEmulator();

$this->tokenMap[self::T_COALESCE_EQUAL] = Tokens::T_COALESCE_EQUAL;
$this->tokenMap[self::T_FN] = Tokens::T_FN;
$this->tokenMap[self::T_MATCH] = Tokens::T_MATCH;
}

public function startLexing(string $code, ErrorHandler $errorHandler = null) {
Expand Down
4 changes: 1 addition & 3 deletions lib/PhpParser/Lexer/TokenEmulator/FnTokenEmulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ public function isEmulationNeeded(string $code) : bool

public function emulate(string $code, array $tokens): array
{
// We need to manually iterate and manage a count because we'll change
// the tokens array on the way
foreach ($tokens as $i => $token) {
if ($token[0] === T_STRING && $token[1] === 'fn') {
if ($token[0] === T_STRING && strtolower($token[1]) === 'fn') {
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $i);
if ($previousNonSpaceToken !== null && $previousNonSpaceToken[0] === T_OBJECT_OPERATOR) {
continue;
Expand Down
51 changes: 51 additions & 0 deletions lib/PhpParser/Lexer/TokenEmulator/MatchTokenEmulator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);

namespace PhpParser\Lexer\TokenEmulator;

use PhpParser\Lexer\Emulative;

final class MatchTokenEmulator implements TokenEmulatorInterface
{
public function isEmulationNeeded(string $code) : bool
{
// skip version where this is supported
if (version_compare(\PHP_VERSION, Emulative::PHP_8_0, '>=')) {
return false;
}

return strpos($code, 'match') !== false;
}

public function emulate(string $code, array $tokens): array
{
foreach ($tokens as $i => $token) {
if ($token[0] === T_STRING && strtolower($token[1]) === 'match') {
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $i);
if ($previousNonSpaceToken !== null && $previousNonSpaceToken[0] === T_OBJECT_OPERATOR) {
continue;
}

$tokens[$i][0] = Emulative::T_MATCH;
}
}

return $tokens;
}

/**
* @param mixed[] $tokens
* @return mixed[]|null
*/
private function getPreviousNonSpaceToken(array $tokens, int $start)
{
for ($i = $start - 1; $i >= 0; --$i) {
if ($tokens[$i][0] === T_WHITESPACE) {
continue;
}

return $tokens[$i];
}

return null;
}
}
31 changes: 31 additions & 0 deletions lib/PhpParser/Node/Expr/Match_.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace PhpParser\Node\Expr;

use PhpParser\Node;
use PhpParser\Node\MatchArm;

class Match_ extends Node\Expr
{
/** @var Node\Expr */
public $cond;
/** @var MatchArm[] */
public $arms;

/**
* @param MatchArm[] $arms
*/
public function __construct(Node\Expr $cond, array $arms = [], array $attributes = []) {
$this->attributes = $attributes;
$this->cond = $cond;
$this->arms = $arms;
}

public function getSubNodeNames() : array {
return ['cond', 'arms'];
}

public function getType() : string {
return 'Expr_Match';
}
}
31 changes: 31 additions & 0 deletions lib/PhpParser/Node/MatchArm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace PhpParser\Node;

use PhpParser\Node;
use PhpParser\NodeAbstract;

class MatchArm extends NodeAbstract
{
/** @var null|Node\Expr[] */
public $conds;
/** @var Node\Expr */
public $body;

/**
* @param null|Node\Expr[] $conds
*/
public function __construct($conds, Node\Expr $body, array $attributes = []) {
$this->conds = $conds;
$this->body = $body;
$this->attributes = $attributes;
}

public function getSubNodeNames() : array {
return ['conds', 'body'];
}

public function getType() : string {
return 'MatchArm';
}
}
Loading

0 comments on commit 69c5d48

Please sign in to comment.