Add support for nullsafe operator

This commit is contained in:
Nikita Popov 2020-08-02 10:30:44 +02:00
parent 31be7b4ed9
commit 23d9c17770
15 changed files with 953 additions and 659 deletions

View File

@ -943,6 +943,8 @@ callable_variable:
| function_call { $$ = $1; }
| array_object_dereferencable T_OBJECT_OPERATOR property_name argument_list
{ $$ = Expr\MethodCall[$1, $3, $4]; }
| array_object_dereferencable T_NULLSAFE_OBJECT_OPERATOR property_name argument_list
{ $$ = Expr\NullsafeMethodCall[$1, $3, $4]; }
;
optional_plain_variable:
@ -955,6 +957,8 @@ variable:
| static_member { $$ = $1; }
| array_object_dereferencable T_OBJECT_OPERATOR property_name
{ $$ = Expr\PropertyFetch[$1, $3]; }
| array_object_dereferencable T_NULLSAFE_OBJECT_OPERATOR property_name
{ $$ = Expr\NullsafePropertyFetch[$1, $3]; }
;
simple_variable:
@ -979,6 +983,7 @@ new_variable:
| new_variable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| new_variable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| new_variable T_OBJECT_OPERATOR property_name { $$ = Expr\PropertyFetch[$1, $3]; }
| new_variable T_NULLSAFE_OBJECT_OPERATOR property_name { $$ = Expr\NullsafePropertyFetch[$1, $3]; }
| class_name T_PAAMAYIM_NEKUDOTAYIM static_member_prop_name
{ $$ = Expr\StaticPropertyFetch[$1, $3]; }
| new_variable T_PAAMAYIM_NEKUDOTAYIM static_member_prop_name
@ -1048,6 +1053,7 @@ encaps_var:
plain_variable { $$ = $1; }
| plain_variable '[' encaps_var_offset ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| plain_variable T_OBJECT_OPERATOR identifier { $$ = Expr\PropertyFetch[$1, $3]; }
| plain_variable T_NULLSAFE_OBJECT_OPERATOR identifier { $$ = Expr\NullsafePropertyFetch[$1, $3]; }
| T_DOLLAR_OPEN_CURLY_BRACES expr '}' { $$ = Expr\Variable[$2]; }
| T_DOLLAR_OPEN_CURLY_BRACES T_STRING_VARNAME '}' { $$ = Expr\Variable[$2]; }
| T_DOLLAR_OPEN_CURLY_BRACES encaps_str_varname '[' expr ']' '}'

View File

@ -85,6 +85,7 @@
%token T_EXTENDS
%token T_IMPLEMENTS
%token T_OBJECT_OPERATOR
%token T_NULLSAFE_OBJECT_OPERATOR
%token T_DOUBLE_ARROW
%token T_LIST
%token T_ARRAY

View File

@ -428,6 +428,9 @@ class Lexer
if (!defined('T_MATCH')) {
\define('T_MATCH', -7);
}
if (!defined('T_NULLSAFE_OBJECT_OPERATOR')) {
\define('T_NULLSAFE_OBJECT_OPERATOR', -8);
}
}
/**
@ -481,6 +484,7 @@ class Lexer
$tokenMap[\T_NAME_FULLY_QUALIFIED] = Tokens::T_NAME_FULLY_QUALIFIED;
$tokenMap[\T_NAME_RELATIVE] = Tokens::T_NAME_RELATIVE;
$tokenMap[\T_MATCH] = Tokens::T_MATCH;
$tokenMap[\T_NULLSAFE_OBJECT_OPERATOR] = Tokens::T_NULLSAFE_OBJECT_OPERATOR;
return $tokenMap;
}

View File

@ -8,6 +8,7 @@ use PhpParser\Lexer;
use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NullsafeTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
use PhpParser\Lexer\TokenEmulator\TokenEmulatorInterface;
use PhpParser\Parser\Tokens;
@ -49,6 +50,7 @@ REGEX;
$this->tokenEmulators[] = new MatchTokenEmulator();
$this->tokenEmulators[] = new CoaleseEqualTokenEmulator();
$this->tokenEmulators[] = new NumericLiteralSeparatorEmulator();
$this->tokenEmulators[] = new NullsafeTokenEmulator();
}
public function startLexing(string $code, ErrorHandler $errorHandler = null) {

View File

@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace PhpParser\Lexer\TokenEmulator;
use PhpParser\Lexer\Emulative;
final class NullsafeTokenEmulator implements TokenEmulatorInterface
{
public function getPhpVersion(): string
{
return Emulative::PHP_8_0;
}
public function isEmulationNeeded(string $code): bool
{
return strpos($code, '?->') !== false;
}
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
$line = 1;
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
if (isset($tokens[$i + 1])) {
if ($tokens[$i] === '?' && $tokens[$i + 1][0] === \T_OBJECT_OPERATOR) {
array_splice($tokens, $i, 2, [
[\T_NULLSAFE_OBJECT_OPERATOR, '?->', $line]
]);
$c--;
continue;
}
}
if (\is_array($tokens[$i])) {
$line += substr_count($tokens[$i][1], "\n");
}
}
return $tokens;
}
public function reverseEmulate(string $code, array $tokens): array
{
// ?-> was not valid code previously, don't bother.
return $tokens;
}
}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Expr;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
class NullsafeMethodCall extends Expr
{
/** @var Expr Variable holding object */
public $var;
/** @var Identifier|Expr Method name */
public $name;
/** @var Arg[] Arguments */
public $args;
/**
* Constructs a nullsafe method call node.
*
* @param Expr $var Variable holding object
* @param string|Identifier|Expr $name Method name
* @param Arg[] $args Arguments
* @param array $attributes Additional attributes
*/
public function __construct(Expr $var, $name, array $args = [], array $attributes = []) {
$this->attributes = $attributes;
$this->var = $var;
$this->name = \is_string($name) ? new Identifier($name) : $name;
$this->args = $args;
}
public function getSubNodeNames() : array {
return ['var', 'name', 'args'];
}
public function getType() : string {
return 'Expr_NullsafeMethodCall';
}
}

View File

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Expr;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
class NullsafePropertyFetch extends Expr
{
/** @var Expr Variable holding object */
public $var;
/** @var Identifier|Expr Property name */
public $name;
/**
* Constructs a nullsafe property fetch node.
*
* @param Expr $var Variable holding object
* @param string|Identifier|Expr $name Property name
* @param array $attributes Additional attributes
*/
public function __construct(Expr $var, $name, array $attributes = []) {
$this->attributes = $attributes;
$this->var = $var;
$this->name = \is_string($name) ? new Identifier($name) : $name;
}
public function getSubNodeNames() : array {
return ['var', 'name'];
}
public function getType() : string {
return 'Expr_NullsafePropertyFetch';
}
}

View File

@ -17,11 +17,11 @@ use PhpParser\Node\Stmt;
*/
class Php5 extends \PhpParser\ParserAbstract
{
protected $tokenToSymbolMapSize = 390;
protected $tokenToSymbolMapSize = 391;
protected $actionTableSize = 1061;
protected $gotoTableSize = 580;
protected $invalidSymbol = 163;
protected $invalidSymbol = 164;
protected $errorSymbol = 1;
protected $defaultAction = -32766;
protected $unexpectedTokenRule = 32767;
@ -192,36 +192,37 @@ class Php5 extends \PhpParser\ParserAbstract
"'$'",
"'`'",
"']'",
"'\"'"
"'\"'",
"T_NULLSAFE_OBJECT_OPERATOR"
);
protected $tokenToSymbol = array(
0, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 54, 162, 163, 159, 53, 36, 163,
157, 158, 51, 48, 7, 49, 50, 52, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 30, 154,
42, 15, 44, 29, 66, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 68, 163, 161, 35, 163, 160, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 155, 34, 156, 56, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 163, 163, 163, 163,
163, 163, 163, 163, 163, 163, 1, 2, 3, 4,
0, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 54, 162, 164, 159, 53, 36, 164,
157, 158, 51, 48, 7, 49, 50, 52, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 30, 154,
42, 15, 44, 29, 66, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 68, 164, 161, 35, 164, 160, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 155, 34, 156, 56, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 164, 164, 164, 164,
164, 164, 164, 164, 164, 164, 1, 2, 3, 4,
5, 6, 8, 9, 10, 11, 12, 13, 14, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 31, 32, 33, 37, 38, 39, 40, 41,
@ -232,9 +233,10 @@ class Php5 extends \PhpParser\ParserAbstract
94, 95, 96, 97, 98, 99, 100, 101, 102, 103,
104, 105, 106, 107, 108, 109, 110, 111, 112, 113,
114, 115, 116, 117, 118, 119, 120, 121, 122, 123,
124, 125, 126, 127, 128, 129, 130, 131, 132, 133,
134, 135, 136, 137, 138, 139, 140, 141, 142, 143,
144, 145, 146, 147, 148, 149, 150, 151, 152, 153
124, 125, 126, 127, 128, 129, 130, 131, 163, 132,
133, 134, 135, 136, 137, 138, 139, 140, 141, 142,
143, 144, 145, 146, 147, 148, 149, 150, 151, 152,
153
);
protected $action = array(

File diff suppressed because it is too large Load Diff

View File

@ -117,26 +117,27 @@ final class Tokens
const T_EXTENDS = 365;
const T_IMPLEMENTS = 366;
const T_OBJECT_OPERATOR = 367;
const T_LIST = 368;
const T_ARRAY = 369;
const T_CALLABLE = 370;
const T_CLASS_C = 371;
const T_TRAIT_C = 372;
const T_METHOD_C = 373;
const T_FUNC_C = 374;
const T_LINE = 375;
const T_FILE = 376;
const T_START_HEREDOC = 377;
const T_END_HEREDOC = 378;
const T_DOLLAR_OPEN_CURLY_BRACES = 379;
const T_CURLY_OPEN = 380;
const T_PAAMAYIM_NEKUDOTAYIM = 381;
const T_NAMESPACE = 382;
const T_NS_C = 383;
const T_DIR = 384;
const T_NS_SEPARATOR = 385;
const T_ELLIPSIS = 386;
const T_NAME_FULLY_QUALIFIED = 387;
const T_NAME_QUALIFIED = 388;
const T_NAME_RELATIVE = 389;
const T_NULLSAFE_OBJECT_OPERATOR = 368;
const T_LIST = 369;
const T_ARRAY = 370;
const T_CALLABLE = 371;
const T_CLASS_C = 372;
const T_TRAIT_C = 373;
const T_METHOD_C = 374;
const T_FUNC_C = 375;
const T_LINE = 376;
const T_FILE = 377;
const T_START_HEREDOC = 378;
const T_END_HEREDOC = 379;
const T_DOLLAR_OPEN_CURLY_BRACES = 380;
const T_CURLY_OPEN = 381;
const T_PAAMAYIM_NEKUDOTAYIM = 382;
const T_NAMESPACE = 383;
const T_NS_C = 384;
const T_DIR = 385;
const T_NS_SEPARATOR = 386;
const T_ELLIPSIS = 387;
const T_NAME_FULLY_QUALIFIED = 388;
const T_NAME_QUALIFIED = 389;
const T_NAME_RELATIVE = 390;
}

View File

@ -492,6 +492,11 @@ class Standard extends PrettyPrinterAbstract
. '(' . $this->pMaybeMultiline($node->args) . ')';
}
protected function pExpr_NullsafeMethodCall(Expr\NUllsafeMethodCall $node) {
return $this->pDereferenceLhs($node->var) . '?->' . $this->pObjectProperty($node->name)
. '(' . $this->pMaybeMultiline($node->args) . ')';
}
protected function pExpr_StaticCall(Expr\StaticCall $node) {
return $this->pDereferenceLhs($node->class) . '::'
. ($node->name instanceof Expr
@ -577,6 +582,10 @@ class Standard extends PrettyPrinterAbstract
return $this->pDereferenceLhs($node->var) . '->' . $this->pObjectProperty($node->name);
}
protected function pExpr_NullsafePropertyFetch(Expr\NullsafePropertyFetch $node) {
return $this->pDereferenceLhs($node->var) . '?->' . $this->pObjectProperty($node->name);
}
protected function pExpr_StaticPropertyFetch(Expr\StaticPropertyFetch $node) {
return $this->pDereferenceLhs($node->class) . '::$' . $this->pObjectProperty($node->name);
}

View File

@ -999,6 +999,7 @@ abstract class PrettyPrinterAbstract
|| $node instanceof Expr\ArrayDimFetch
|| $node instanceof Expr\FuncCall
|| $node instanceof Expr\MethodCall
|| $node instanceof Expr\NullsafeMethodCall
|| $node instanceof Expr\StaticCall
|| $node instanceof Expr\Array_);
}
@ -1015,9 +1016,11 @@ abstract class PrettyPrinterAbstract
|| $node instanceof Node\Name
|| $node instanceof Expr\ArrayDimFetch
|| $node instanceof Expr\PropertyFetch
|| $node instanceof Expr\NullsafePropertyFetch
|| $node instanceof Expr\StaticPropertyFetch
|| $node instanceof Expr\FuncCall
|| $node instanceof Expr\MethodCall
|| $node instanceof Expr\NullsafeMethodCall
|| $node instanceof Expr\StaticCall
|| $node instanceof Expr\Array_
|| $node instanceof Scalar\String_
@ -1138,6 +1141,10 @@ abstract class PrettyPrinterAbstract
'var' => self::FIXUP_DEREF_LHS,
'name' => self::FIXUP_BRACED_NAME,
],
Expr\NullsafeMethodCall::class => [
'var' => self::FIXUP_DEREF_LHS,
'name' => self::FIXUP_BRACED_NAME,
],
Expr\StaticPropertyFetch::class => [
'class' => self::FIXUP_DEREF_LHS,
'name' => self::FIXUP_VAR_BRACED_NAME,
@ -1146,6 +1153,10 @@ abstract class PrettyPrinterAbstract
'var' => self::FIXUP_DEREF_LHS,
'name' => self::FIXUP_BRACED_NAME,
],
Expr\NullsafePropertyFetch::class => [
'var' => self::FIXUP_DEREF_LHS,
'name' => self::FIXUP_BRACED_NAME,
],
Scalar\Encapsed::class => [
'parts' => self::FIXUP_ENCAPSED,
],
@ -1303,6 +1314,7 @@ abstract class PrettyPrinterAbstract
'Expr_Isset->vars' => ', ',
'Expr_List->items' => ', ',
'Expr_MethodCall->args' => ', ',
'Expr_NullsafeMethodCall->args' => ', ',
'Expr_New->args' => ', ',
'Expr_PrintableNewAnonClass->args' => ', ',
'Expr_StaticCall->args' => ', ',
@ -1370,6 +1382,7 @@ abstract class PrettyPrinterAbstract
'Expr_Closure->params' => ['(', '', ''],
'Expr_FuncCall->args' => ['(', '', ''],
'Expr_MethodCall->args' => ['(', '', ''],
'Expr_NullsafeMethodCall->args' => ['(', '', ''],
'Expr_New->args' => ['(', '', ''],
'Expr_PrintableNewAnonClass->args' => ['(', '', ''],
'Expr_PrintableNewAnonClass->implements' => [null, ' implements ', ''],

View File

@ -48,6 +48,18 @@ class EmulativeTest extends LexerTest
$this->assertSame(0, $lexer->getNextToken());
}
/**
* @dataProvider provideTestReplaceKeywords
*/
public function testNoReplaceKeywordsAfterNullsafeObjectOperator(string $keyword) {
$lexer = $this->getLexer();
$lexer->startLexing('<?php ?->' . $keyword);
$this->assertSame(Tokens::T_NULLSAFE_OBJECT_OPERATOR, $lexer->getNextToken());
$this->assertSame(Tokens::T_STRING, $lexer->getNextToken());
$this->assertSame(0, $lexer->getNextToken());
}
public function provideTestReplaceKeywords() {
return [
// PHP 8.0
@ -260,6 +272,9 @@ class EmulativeTest extends LexerTest
[Tokens::T_LNUMBER, '1_0'],
[Tokens::T_STRING, 'abc'],
]],
['?->', [
[Tokens::T_NULLSAFE_OBJECT_OPERATOR, '?->'],
]],
];
}

View File

@ -0,0 +1,82 @@
Nullsafe operator
-----
<?php
$a?->b;
$a?->b($c);
new $a?->b;
"{$a?->b}";
"$a?->b";
-----
!!php7
array(
0: Stmt_Expression(
expr: Expr_NullsafePropertyFetch(
var: Expr_Variable(
name: a
)
name: Identifier(
name: b
)
)
)
1: Stmt_Expression(
expr: Expr_NullsafeMethodCall(
var: Expr_Variable(
name: a
)
name: Identifier(
name: b
)
args: array(
0: Arg(
value: Expr_Variable(
name: c
)
byRef: false
unpack: false
)
)
)
)
2: Stmt_Expression(
expr: Expr_New(
class: Expr_NullsafePropertyFetch(
var: Expr_Variable(
name: a
)
name: Identifier(
name: b
)
)
args: array(
)
)
)
3: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_NullsafePropertyFetch(
var: Expr_Variable(
name: a
)
name: Identifier(
name: b
)
)
)
)
)
4: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: a
)
1: Scalar_EncapsedStringPart(
value: ?->b
)
)
)
)
)

View File

@ -0,0 +1,22 @@
Nullsafe operator
-----
<?php
$a?->b;
$a?->b($c);
$a?->b?->c;
$a?->b($c)?->d;
$a?->b($c)();
new $a?->b;
"{$a?->b}";
"$a?->b";
-----
!!php7
$a?->b;
$a?->b($c);
$a?->b?->c;
$a?->b($c)?->d;
$a?->b($c)();
new $a?->b();
"{$a?->b}";
"{$a}?->b";