Skip to content

Commit f3b19c1

Browse files
TomasVotrubanikic
authored andcommitted
[PHP 7.4] Add support for arrow functions (nikic#602)
Per RFC https://wiki.php.net/rfc/arrow_functions_v2.
1 parent 78d9985 commit f3b19c1

File tree

20 files changed

+2673
-2184
lines changed

20 files changed

+2673
-2184
lines changed

grammar/php5.y

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ reserved_non_modifiers:
2727
| T_FINALLY | T_THROW | T_USE | T_INSTEADOF | T_GLOBAL | T_VAR | T_UNSET | T_ISSET | T_EMPTY | T_CONTINUE | T_GOTO
2828
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
2929
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
30-
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER
30+
| 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
3131
;
3232

3333
semi_reserved:

grammar/php7.y

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ reserved_non_modifiers:
2727
| T_FINALLY | T_THROW | T_USE | T_INSTEADOF | T_GLOBAL | T_VAR | T_UNSET | T_ISSET | T_EMPTY | T_CONTINUE | T_GOTO
2828
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
2929
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
30-
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER
30+
| 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
3131
;
3232

3333
semi_reserved:
@@ -729,6 +729,12 @@ expr:
729729
| T_YIELD expr { $$ = Expr\Yield_[$2, null]; }
730730
| T_YIELD expr T_DOUBLE_ARROW expr { $$ = Expr\Yield_[$4, $2]; }
731731
| T_YIELD_FROM expr { $$ = Expr\YieldFrom[$2]; }
732+
733+
| T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
734+
{ $$ = Expr\ArrowFunction[['static' => false, 'byRef' => $2, 'params' => $4, 'returnType' => $6, 'expr' => $8]]; }
735+
| T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
736+
{ $$ = Expr\ArrowFunction[['static' => true, 'byRef' => $3, 'params' => $5, 'returnType' => $7, 'expr' => $9]]; }
737+
732738
| T_FUNCTION optional_ref '(' parameter_list ')' lexical_vars optional_return_type
733739
block_or_error
734740
{ $$ = Expr\Closure[['static' => false, 'byRef' => $2, 'params' => $4, 'uses' => $6, 'returnType' => $7, 'stmts' => $8]]; }

grammar/tokens.y

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
%token T_CONTINUE
6565
%token T_GOTO
6666
%token T_FUNCTION
67+
%token T_FN
6768
%token T_CONST
6869
%token T_RETURN
6970
%token T_TRY

lib/PhpParser/Lexer/Emulative.php

Lines changed: 28 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
use PhpParser\Error;
66
use PhpParser\ErrorHandler;
7-
use PhpParser\Parser;
7+
use PhpParser\Lexer;
8+
use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
9+
use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
10+
use PhpParser\Lexer\TokenEmulator\TokenEmulatorInterface;
811

9-
class Emulative extends \PhpParser\Lexer
12+
class Emulative extends Lexer
1013
{
1114
const PHP_7_3 = '7.3.0dev';
1215
const PHP_7_4 = '7.4.0dev';
@@ -17,22 +20,27 @@ class Emulative extends \PhpParser\Lexer
1720
(?<indentation>\h*)\2(?![a-zA-Z_\x80-\xff])(?<separator>(?:;?[\r\n])?)/x
1821
REGEX;
1922

20-
const T_COALESCE_EQUAL = 1007;
21-
22-
/**
23-
* @var mixed[] Patches used to reverse changes introduced in the code
24-
*/
23+
/** @var mixed[] Patches used to reverse changes introduced in the code */
2524
private $patches = [];
2625

26+
/** @var TokenEmulatorInterface[] */
27+
private $tokenEmulators = [];
28+
2729
/**
2830
* @param mixed[] $options
2931
*/
3032
public function __construct(array $options = [])
3133
{
3234
parent::__construct($options);
3335

36+
// prepare token emulators
37+
$this->tokenEmulators[] = new FnTokenEmulator();
38+
$this->tokenEmulators[] = new CoaleseEqualTokenEmulator();
39+
3440
// add emulated tokens here
35-
$this->tokenMap[self::T_COALESCE_EQUAL] = Parser\Tokens::T_COALESCE_EQUAL;
41+
foreach ($this->tokenEmulators as $emulativeToken) {
42+
$this->tokenMap[$emulativeToken->getTokenId()] = $emulativeToken->getParserTokenId();
43+
}
3644
}
3745

3846
public function startLexing(string $code, ErrorHandler $errorHandler = null) {
@@ -50,8 +58,13 @@ public function startLexing(string $code, ErrorHandler $errorHandler = null) {
5058
$preparedCode = $this->processHeredocNowdoc($code);
5159
parent::startLexing($preparedCode, $collector);
5260

53-
// 2. emulation of ??= token
54-
$this->processCoaleseEqual($code);
61+
// add token emulation
62+
foreach ($this->tokenEmulators as $emulativeToken) {
63+
if ($emulativeToken->isEmulationNeeded($code)) {
64+
$this->tokens = $emulativeToken->emulate($code, $this->tokens);
65+
}
66+
}
67+
5568
$this->fixupTokens();
5669

5770
$errors = $collector->getErrors();
@@ -63,41 +76,6 @@ public function startLexing(string $code, ErrorHandler $errorHandler = null) {
6376
}
6477
}
6578

66-
private function isCoalesceEqualEmulationNeeded(string $code): bool
67-
{
68-
// skip version where this works without emulation
69-
if (version_compare(\PHP_VERSION, self::PHP_7_4, '>=')) {
70-
return false;
71-
}
72-
73-
return strpos($code, '??=') !== false;
74-
}
75-
76-
private function processCoaleseEqual(string $code)
77-
{
78-
if ($this->isCoalesceEqualEmulationNeeded($code) === false) {
79-
return;
80-
}
81-
82-
// We need to manually iterate and manage a count because we'll change
83-
// the tokens array on the way
84-
$line = 1;
85-
for ($i = 0, $c = count($this->tokens); $i < $c; ++$i) {
86-
if (isset($this->tokens[$i + 1])) {
87-
if ($this->tokens[$i][0] === T_COALESCE && $this->tokens[$i + 1] === '=') {
88-
array_splice($this->tokens, $i, 2, [
89-
[self::T_COALESCE_EQUAL, '??=', $line]
90-
]);
91-
$c--;
92-
continue;
93-
}
94-
}
95-
if (\is_array($this->tokens[$i])) {
96-
$line += substr_count($this->tokens[$i][1], "\n");
97-
}
98-
}
99-
}
100-
10179
private function isHeredocNowdocEmulationNeeded(string $code): bool
10280
{
10381
// skip version where this works without emulation
@@ -155,15 +133,13 @@ private function processHeredocNowdoc(string $code): string
155133

156134
private function isEmulationNeeded(string $code): bool
157135
{
158-
if ($this->isHeredocNowdocEmulationNeeded($code)) {
159-
return true;
160-
}
161-
162-
if ($this->isCoalesceEqualEmulationNeeded($code)) {
163-
return true;
136+
foreach ($this->tokenEmulators as $emulativeToken) {
137+
if ($emulativeToken->isEmulationNeeded($code)) {
138+
return true;
139+
}
164140
}
165141

166-
return false;
142+
return $this->isHeredocNowdocEmulationNeeded($code);
167143
}
168144

169145
private function fixupTokens()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Lexer\TokenEmulator;
4+
5+
use PhpParser\Lexer\Emulative;
6+
use PhpParser\Parser\Tokens;
7+
8+
final class CoaleseEqualTokenEmulator implements TokenEmulatorInterface
9+
{
10+
const T_COALESCE_EQUAL = 1007;
11+
12+
public function getTokenId(): int
13+
{
14+
return self::T_COALESCE_EQUAL;
15+
}
16+
17+
public function getParserTokenId(): int
18+
{
19+
return Tokens::T_COALESCE_EQUAL;
20+
}
21+
22+
public function isEmulationNeeded(string $code) : bool
23+
{
24+
// skip version where this is supported
25+
if (version_compare(\PHP_VERSION, Emulative::PHP_7_4, '>=')) {
26+
return false;
27+
}
28+
29+
return strpos($code, '??=') !== false;
30+
}
31+
32+
public function emulate(string $code, array $tokens): array
33+
{
34+
// We need to manually iterate and manage a count because we'll change
35+
// the tokens array on the way
36+
$line = 1;
37+
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
38+
if (isset($tokens[$i + 1])) {
39+
if ($tokens[$i][0] === T_COALESCE && $tokens[$i + 1] === '=') {
40+
array_splice($tokens, $i, 2, [
41+
[self::T_COALESCE_EQUAL, '??=', $line]
42+
]);
43+
$c--;
44+
continue;
45+
}
46+
}
47+
if (\is_array($tokens[$i])) {
48+
$line += substr_count($tokens[$i][1], "\n");
49+
}
50+
}
51+
52+
return $tokens;
53+
}
54+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Lexer\TokenEmulator;
4+
5+
use PhpParser\Lexer\Emulative;
6+
use PhpParser\Parser\Tokens;
7+
8+
final class FnTokenEmulator implements TokenEmulatorInterface
9+
{
10+
const T_FN = 1008;
11+
12+
public function getTokenId(): int
13+
{
14+
return self::T_FN;
15+
}
16+
17+
public function getParserTokenId(): int
18+
{
19+
return Tokens::T_FN;
20+
}
21+
22+
public function isEmulationNeeded(string $code) : bool
23+
{
24+
// skip version where this is supported
25+
if (version_compare(\PHP_VERSION, Emulative::PHP_7_4, '>=')) {
26+
return false;
27+
}
28+
29+
return strpos($code, 'fn') !== false;
30+
}
31+
32+
public function emulate(string $code, array $tokens): array
33+
{
34+
// We need to manually iterate and manage a count because we'll change
35+
// the tokens array on the way
36+
foreach ($tokens as $i => $token) {
37+
if ($token[0] === T_STRING && $token[1] === 'fn') {
38+
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $i);
39+
if ($previousNonSpaceToken !== null && $previousNonSpaceToken[0] === T_OBJECT_OPERATOR) {
40+
continue;
41+
}
42+
43+
$tokens[$i][0] = self::T_FN;
44+
}
45+
}
46+
47+
return $tokens;
48+
}
49+
50+
/**
51+
* @param mixed[] $tokens
52+
* @return mixed[]|null
53+
*/
54+
private function getPreviousNonSpaceToken(array $tokens, int $start)
55+
{
56+
for ($i = $start - 1; $i >= 0; --$i) {
57+
if ($tokens[$i][0] === T_WHITESPACE) {
58+
continue;
59+
}
60+
61+
return $tokens[$i];
62+
}
63+
64+
return null;
65+
}
66+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Lexer\TokenEmulator;
4+
5+
interface TokenEmulatorInterface
6+
{
7+
public function getTokenId(): int;
8+
9+
public function getParserTokenId(): int;
10+
11+
public function isEmulationNeeded(string $code): bool;
12+
13+
/**
14+
* @return array Modified Tokens
15+
*/
16+
public function emulate(string $code, array $tokens): array;
17+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Node\Expr;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\FunctionLike;
8+
9+
class ArrowFunction extends Expr implements FunctionLike
10+
{
11+
/** @var bool */
12+
public $static;
13+
14+
/** @var bool */
15+
public $byRef;
16+
17+
/** @var Node\Param[] */
18+
public $params = [];
19+
20+
/** @var null|Node\Identifier|Node\Name|Node\NullableType */
21+
public $returnType;
22+
23+
/** @var Expr */
24+
public $expr;
25+
26+
/**
27+
* @param array $subNodes Array of the following optional subnodes:
28+
* 'static' => false : Whether the closure is static
29+
* 'byRef' => false : Whether to return by reference
30+
* 'params' => array() : Parameters
31+
* 'returnType' => null : Return type
32+
* 'expr' => Expr : Expression body
33+
* @param array $attributes Additional attributes
34+
*/
35+
public function __construct(array $subNodes = [], array $attributes = []) {
36+
parent::__construct($attributes);
37+
$this->static = $subNodes['static'] ?? false;
38+
$this->byRef = $subNodes['byRef'] ?? false;
39+
$this->params = $subNodes['params'] ?? [];
40+
$returnType = $subNodes['returnType'] ?? null;
41+
$this->returnType = \is_string($returnType) ? new Node\Identifier($returnType) : $returnType;
42+
$this->expr = $subNodes['expr'] ?? null;
43+
}
44+
45+
public function getSubNodeNames() : array {
46+
return ['static', 'byRef', 'params', 'returnType', 'expr'];
47+
}
48+
49+
public function returnsByRef() : bool {
50+
return $this->byRef;
51+
}
52+
53+
public function getParams() : array {
54+
return $this->params;
55+
}
56+
57+
public function getReturnType() {
58+
return $this->returnType;
59+
}
60+
61+
/**
62+
* @return Node\Stmt\Return_[]
63+
*/
64+
public function getStmts() : array {
65+
return [new Node\Stmt\Return_($this->expr)];
66+
}
67+
68+
public function getType() : string {
69+
return 'Expr_ArrowFunction';
70+
}
71+
}

lib/PhpParser/Node/Name.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
class Name extends NodeAbstract
88
{
9-
/**
10-
* @var string[] Parts of the name
11-
*/
9+
/** @var string[] Parts of the name */
1210
public $parts;
1311

1412
private static $specialClassNames = [
@@ -237,7 +235,7 @@ private static function prepareName($name) : array {
237235
'Expected string, array of parts or Name instance'
238236
);
239237
}
240-
238+
241239
public function getType() : string {
242240
return 'Name';
243241
}

0 commit comments

Comments
 (0)