Add support for new PHP 8.1 modifiers (#796)

Implement support for readonly properties (https://wiki.php.net/rfc/readonly_properties_v2) and
final class contstants (https://wiki.php.net/rfc/final_class_const).
This commit is contained in:
Máté Kocsis 2021-07-21 12:43:29 +02:00 committed by GitHub
parent c4304c76bd
commit 55c4269232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1812 additions and 1566 deletions

View File

@ -38,7 +38,7 @@ reserved_non_modifiers:
semi_reserved:
reserved_non_modifiers
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC | T_READONLY
;
identifier_ex:
@ -535,6 +535,7 @@ optional_visibility_modifier:
| T_PUBLIC { $$ = Stmt\Class_::MODIFIER_PUBLIC; }
| T_PROTECTED { $$ = Stmt\Class_::MODIFIER_PROTECTED; }
| T_PRIVATE { $$ = Stmt\Class_::MODIFIER_PRIVATE; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
;
parameter:
@ -725,6 +726,7 @@ member_modifier:
| T_STATIC { $$ = Stmt\Class_::MODIFIER_STATIC; }
| T_ABSTRACT { $$ = Stmt\Class_::MODIFIER_ABSTRACT; }
| T_FINAL { $$ = Stmt\Class_::MODIFIER_FINAL; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
;
property_declaration_list:

View File

@ -74,7 +74,7 @@
%token T_USE
%token T_INSTEADOF
%token T_GLOBAL
%right T_STATIC T_ABSTRACT T_FINAL T_PRIVATE T_PROTECTED T_PUBLIC
%right T_STATIC T_ABSTRACT T_FINAL T_PRIVATE T_PROTECTED T_PUBLIC T_READONLY
%token T_VAR
%token T_UNSET
%token T_ISSET

View File

@ -77,6 +77,17 @@ class ClassConst implements PhpParser\Builder
return $this;
}
/**
* Makes the constant final.
*
* @return $this The builder instance (for fluid interface)
*/
public function makeFinal() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_FINAL);
return $this;
}
/**
* Sets doc comment for the constant.
*

View File

@ -77,6 +77,17 @@ class Property implements PhpParser\Builder
return $this;
}
/**
* Makes the property readonly.
*
* @return $this The builder instance (for fluid interface)
*/
public function makeReadonly() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_READONLY);
return $this;
}
/**
* Sets default value for the property.
*

View File

@ -443,6 +443,7 @@ class Lexer
'T_ENUM',
'T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG',
'T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG',
'T_READONLY',
];
// PHP-Parser might be used together with another library that also emulates some or all
@ -536,6 +537,7 @@ class Lexer
$tokenMap[\T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG] = Tokens::T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG;
$tokenMap[\T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG] = Tokens::T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG;
$tokenMap[\T_ENUM] = Tokens::T_ENUM;
$tokenMap[\T_READONLY] = Tokens::T_READONLY;
return $tokenMap;
}
@ -544,7 +546,7 @@ class Lexer
// Based on semi_reserved production.
return array_fill_keys([
\T_STRING,
\T_STATIC, \T_ABSTRACT, \T_FINAL, \T_PRIVATE, \T_PROTECTED, \T_PUBLIC,
\T_STATIC, \T_ABSTRACT, \T_FINAL, \T_PRIVATE, \T_PROTECTED, \T_PUBLIC, \T_READONLY,
\T_INCLUDE, \T_INCLUDE_ONCE, \T_EVAL, \T_REQUIRE, \T_REQUIRE_ONCE, \T_LOGICAL_OR, \T_LOGICAL_XOR, \T_LOGICAL_AND,
\T_INSTANCEOF, \T_NEW, \T_CLONE, \T_EXIT, \T_IF, \T_ELSEIF, \T_ELSE, \T_ENDIF, \T_ECHO, \T_DO, \T_WHILE,
\T_ENDWHILE, \T_FOR, \T_ENDFOR, \T_FOREACH, \T_ENDFOREACH, \T_DECLARE, \T_ENDDECLARE, \T_AS, \T_TRY, \T_CATCH,

View File

@ -13,6 +13,7 @@ use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NullsafeTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
use PhpParser\Lexer\TokenEmulator\ReadonlyTokenEmulator;
use PhpParser\Lexer\TokenEmulator\ReverseEmulator;
use PhpParser\Lexer\TokenEmulator\TokenEmulator;
@ -53,6 +54,7 @@ class Emulative extends Lexer
new NullsafeTokenEmulator(),
new AttributeEmulator(),
new EnumTokenEmulator(),
new ReadonlyTokenEmulator(),
];
// Collect emulators that are relevant for the PHP version we're running

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace PhpParser\Lexer\TokenEmulator;
use PhpParser\Lexer\Emulative;
final class ReadonlyTokenEmulator extends KeywordEmulator
{
public function getPhpVersion(): string
{
return Emulative::PHP_8_1;
}
public function getKeywordString(): string
{
return 'readonly';
}
public function getKeywordToken(): int
{
return \T_READONLY;
}
}

View File

@ -65,6 +65,15 @@ class ClassConst extends Node\Stmt
return (bool) ($this->flags & Class_::MODIFIER_PRIVATE);
}
/**
* Whether constant is final.
*
* @return bool
*/
public function isFinal() : bool {
return (bool) ($this->flags & Class_::MODIFIER_FINAL);
}
public function getType() : string {
return 'Stmt_ClassConst';
}

View File

@ -13,6 +13,7 @@ class Class_ extends ClassLike
const MODIFIER_STATIC = 8;
const MODIFIER_ABSTRACT = 16;
const MODIFIER_FINAL = 32;
const MODIFIER_READONLY = 64;
const VISIBILITY_MODIFIER_MASK = 7; // 1 | 2 | 4
@ -96,6 +97,10 @@ class Class_ extends ClassLike
throw new Error('Multiple final modifiers are not allowed');
}
if ($a & self::MODIFIER_READONLY && $b & self::MODIFIER_READONLY) {
throw new Error('Multiple readonly modifiers are not allowed');
}
if ($a & 48 && $b & 48) {
throw new Error('Cannot use the final modifier on an abstract class member');
}

View File

@ -77,6 +77,15 @@ class Property extends Node\Stmt
return (bool) ($this->flags & Class_::MODIFIER_STATIC);
}
/**
* Whether the property is readonly.
*
* @return bool
*/
public function isReadonly() : bool {
return (bool) ($this->flags & Class_::MODIFIER_READONLY);
}
public function getType() : string {
return 'Stmt_Property';
}

View File

@ -128,6 +128,9 @@ class NodeDumper
if ($flags & Class_::MODIFIER_FINAL) {
$strs[] = 'MODIFIER_FINAL';
}
if ($flags & Class_::MODIFIER_READONLY) {
$strs[] = 'MODIFIER_READONLY';
}
if ($strs) {
return implode(' | ', $strs) . ' (' . $flags . ')';

View File

@ -17,11 +17,11 @@ use PhpParser\Node\Stmt;
*/
class Php5 extends \PhpParser\ParserAbstract
{
protected $tokenToSymbolMapSize = 395;
protected $tokenToSymbolMapSize = 396;
protected $actionTableSize = 1093;
protected $gotoTableSize = 643;
protected $invalidSymbol = 167;
protected $invalidSymbol = 168;
protected $errorSymbol = 1;
protected $defaultAction = -32766;
protected $unexpectedTokenRule = 32767;
@ -194,38 +194,39 @@ class Php5 extends \PhpParser\ParserAbstract
"'`'",
"']'",
"'\"'",
"T_READONLY",
"T_ENUM",
"T_NULLSAFE_OBJECT_OPERATOR",
"T_ATTRIBUTE"
);
protected $tokenToSymbol = array(
0, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 56, 163, 167, 160, 55, 167, 167,
158, 159, 53, 50, 8, 51, 52, 54, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 31, 155,
44, 16, 46, 30, 68, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 70, 167, 162, 36, 167, 161, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 156, 35, 157, 58, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
167, 167, 167, 167, 167, 167, 1, 2, 3, 4,
0, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 56, 163, 168, 160, 55, 168, 168,
158, 159, 53, 50, 8, 51, 52, 54, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 31, 155,
44, 16, 46, 30, 68, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 70, 168, 162, 36, 168, 161, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 156, 35, 157, 58, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 168, 168, 168, 168,
168, 168, 168, 168, 168, 168, 1, 2, 3, 4,
5, 6, 7, 9, 10, 11, 12, 13, 14, 15,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 32, 33, 34, 37, 38, 39, 40,
@ -235,11 +236,11 @@ class Php5 extends \PhpParser\ParserAbstract
83, 84, 85, 86, 87, 88, 89, 90, 91, 92,
93, 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, 164, 130, 131,
132, 165, 133, 134, 135, 136, 137, 138, 139, 140,
141, 142, 143, 144, 145, 146, 147, 148, 149, 150,
151, 152, 153, 154, 166
113, 114, 115, 116, 117, 118, 119, 120, 121, 164,
122, 123, 124, 125, 126, 127, 128, 129, 165, 130,
131, 132, 166, 133, 134, 135, 136, 137, 138, 139,
140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
150, 151, 152, 153, 154, 167
);
protected $action = array(

File diff suppressed because it is too large Load Diff

View File

@ -108,40 +108,41 @@ final class Tokens
const T_PRIVATE = 356;
const T_PROTECTED = 357;
const T_PUBLIC = 358;
const T_VAR = 359;
const T_UNSET = 360;
const T_ISSET = 361;
const T_EMPTY = 362;
const T_HALT_COMPILER = 363;
const T_CLASS = 364;
const T_TRAIT = 365;
const T_INTERFACE = 366;
const T_ENUM = 367;
const T_EXTENDS = 368;
const T_IMPLEMENTS = 369;
const T_OBJECT_OPERATOR = 370;
const T_NULLSAFE_OBJECT_OPERATOR = 371;
const T_LIST = 372;
const T_ARRAY = 373;
const T_CALLABLE = 374;
const T_CLASS_C = 375;
const T_TRAIT_C = 376;
const T_METHOD_C = 377;
const T_FUNC_C = 378;
const T_LINE = 379;
const T_FILE = 380;
const T_START_HEREDOC = 381;
const T_END_HEREDOC = 382;
const T_DOLLAR_OPEN_CURLY_BRACES = 383;
const T_CURLY_OPEN = 384;
const T_PAAMAYIM_NEKUDOTAYIM = 385;
const T_NAMESPACE = 386;
const T_NS_C = 387;
const T_DIR = 388;
const T_NS_SEPARATOR = 389;
const T_ELLIPSIS = 390;
const T_NAME_FULLY_QUALIFIED = 391;
const T_NAME_QUALIFIED = 392;
const T_NAME_RELATIVE = 393;
const T_ATTRIBUTE = 394;
const T_READONLY = 359;
const T_VAR = 360;
const T_UNSET = 361;
const T_ISSET = 362;
const T_EMPTY = 363;
const T_HALT_COMPILER = 364;
const T_CLASS = 365;
const T_TRAIT = 366;
const T_INTERFACE = 367;
const T_ENUM = 368;
const T_EXTENDS = 369;
const T_IMPLEMENTS = 370;
const T_OBJECT_OPERATOR = 371;
const T_NULLSAFE_OBJECT_OPERATOR = 372;
const T_LIST = 373;
const T_ARRAY = 374;
const T_CALLABLE = 375;
const T_CLASS_C = 376;
const T_TRAIT_C = 377;
const T_METHOD_C = 378;
const T_FUNC_C = 379;
const T_LINE = 380;
const T_FILE = 381;
const T_START_HEREDOC = 382;
const T_END_HEREDOC = 383;
const T_DOLLAR_OPEN_CURLY_BRACES = 384;
const T_CURLY_OPEN = 385;
const T_PAAMAYIM_NEKUDOTAYIM = 386;
const T_NAMESPACE = 387;
const T_NS_C = 388;
const T_DIR = 389;
const T_NS_SEPARATOR = 390;
const T_ELLIPSIS = 391;
const T_NAME_FULLY_QUALIFIED = 392;
const T_NAME_QUALIFIED = 393;
const T_NAME_RELATIVE = 394;
const T_ATTRIBUTE = 395;
}

View File

@ -977,6 +977,12 @@ abstract class ParserAbstract implements Parser
break;
}
}
if ($node->flags & Class_::MODIFIER_READONLY) {
$this->emitError(new Error(
sprintf('Method %s() cannot be readonly', $node->name),
$this->getAttributesAt($modifierPos)));
}
}
protected function checkClassConst(ClassConst $node, $modifierPos) {
@ -990,9 +996,9 @@ abstract class ParserAbstract implements Parser
"Cannot use 'abstract' as constant modifier",
$this->getAttributesAt($modifierPos)));
}
if ($node->flags & Class_::MODIFIER_FINAL) {
if ($node->flags & Class_::MODIFIER_READONLY) {
$this->emitError(new Error(
"Cannot use 'final' as constant modifier",
"Cannot use 'readonly' as constant modifier",
$this->getAttributesAt($modifierPos)));
}
}

View File

@ -1074,7 +1074,8 @@ abstract class PrettyPrinterAbstract
. ($modifiers & Stmt\Class_::MODIFIER_PRIVATE ? 'private ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_STATIC ? 'static ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_ABSTRACT ? 'abstract ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_FINAL ? 'final ' : '');
. ($modifiers & Stmt\Class_::MODIFIER_FINAL ? 'final ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_READONLY ? 'readonly ' : '');
}
/**

View File

@ -65,6 +65,21 @@ class ClassConstTest extends \PHPUnit\Framework\TestCase
),
$node
);
$node = $this->createClassConstBuilder("TEST", 1)
->makeFinal()
->getNode()
;
$this->assertEquals(
new Stmt\ClassConst(
[
new Const_("TEST", new LNumber(1) )
],
Stmt\Class_::MODIFIER_FINAL
),
$node
);
}
public function testDocComment() {

View File

@ -66,6 +66,21 @@ class PropertyTest extends \PHPUnit\Framework\TestCase
),
$node
);
$node = $this->createPropertyBuilder('test')
->makeReadonly()
->getNode()
;
$this->assertEquals(
new Stmt\Property(
Stmt\Class_::MODIFIER_READONLY,
[
new Stmt\PropertyProperty('test')
]
),
$node
);
}
public function testDocComment() {

View File

@ -22,6 +22,7 @@ class ClassConstTest extends \PHPUnit\Framework\TestCase
$this->assertTrue($node->isPublic());
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertFalse($node->isFinal());
}
public function provideModifiers() {
@ -29,6 +30,7 @@ class ClassConstTest extends \PHPUnit\Framework\TestCase
['public'],
['protected'],
['private'],
['final'],
];
}
}

View File

@ -23,6 +23,7 @@ class PropertyTest extends \PHPUnit\Framework\TestCase
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertFalse($node->isStatic());
$this->assertFalse($node->isReadonly());
}
public function testStaticImplicitlyPublic() {
@ -31,6 +32,12 @@ class PropertyTest extends \PHPUnit\Framework\TestCase
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertTrue($node->isStatic());
$this->assertFalse($node->isReadonly());
}
public function testReadonly() {
$node = new Property(Class_::MODIFIER_READONLY, []);
$this->assertTrue($node->isReadonly());
}
public function provideModifiers() {

View File

@ -78,11 +78,11 @@ array(
-----
<?php
class A {
final const X = 1;
readonly const X = 1;
}
-----
!!php7
Cannot use 'final' as constant modifier from 3:5 to 3:9
Cannot use 'readonly' as constant modifier from 3:5 to 3:12
array(
0: Stmt_Class(
attrGroups: array(
@ -98,7 +98,7 @@ array(
0: Stmt_ClassConst(
attrGroups: array(
)
flags: MODIFIER_FINAL (32)
flags: MODIFIER_READONLY (64)
consts: array(
0: Const(
name: Identifier(
@ -150,4 +150,4 @@ array(
)
)
)
)
)

View File

@ -7,6 +7,7 @@ class Foo {
public const B = 2;
protected const C = 3;
private const D = 4;
final const E = 5;
}
-----
!!php7
@ -82,6 +83,21 @@ array(
)
)
)
4: Stmt_ClassConst(
attrGroups: array(
)
flags: MODIFIER_FINAL (32)
consts: array(
0: Const(
name: Identifier(
name: E
)
value: Scalar_LNumber(
value: 5
)
)
)
)
)
)
)

View File

@ -66,6 +66,40 @@ array(
)
)
-----
<?php class A { readonly readonly $a; }
-----
!!php7
Multiple readonly modifiers are not allowed from 1:26 to 1:33
array(
0: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
0: Stmt_Property(
attrGroups: array(
)
flags: MODIFIER_READONLY (64)
type: null
props: array(
0: Stmt_PropertyProperty(
name: VarLikeIdentifier(
name: a
)
default: null
)
)
)
)
)
)
-----
<?php class A { abstract abstract function a(); }
-----
Multiple abstract modifiers are not allowed from 1:26 to 1:33
@ -221,6 +255,31 @@ array(
)
)
-----
<?php readonly class A { }
// Type in the partial parse could conceivably be any of 0, 16 or 32
-----
Syntax error, unexpected T_READONLY from 1:7 to 1:14
array(
0: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
)
)
1: Stmt_Nop(
comments: array(
0: // Type in the partial parse could conceivably be any of 0, 16 or 32
)
)
)
-----
<?php class A { abstract $a; }
-----
Properties cannot be declared abstract from 1:17 to 1:24

View File

@ -6,6 +6,7 @@ class A {
public string $a;
protected static D $b;
private ?float $c;
readonly static public ?int $d;
}
-----
!!php7
@ -73,6 +74,24 @@ array(
)
)
)
3: Stmt_Property(
attrGroups: array(
)
flags: MODIFIER_PUBLIC | MODIFIER_STATIC | MODIFIER_READONLY (73)
type: NullableType(
type: Identifier(
name: int
)
)
props: array(
0: Stmt_PropertyProperty(
name: VarLikeIdentifier(
name: d
)
default: null
)
)
)
)
)
)

View File

@ -0,0 +1,35 @@
Methods cannot be readonly
-----
<?php class A { readonly function foo() {} }
-----
!!php7
Method foo() cannot be readonly from 1:17 to 1:24
array(
0: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
0: Stmt_ClassMethod(
attrGroups: array(
)
flags: MODIFIER_READONLY (64)
byRef: false
name: Identifier(
name: foo
)
params: array(
)
returnType: null
stmts: array(
)
)
)
)
)

View File

@ -8,6 +8,7 @@ class A
public string $b;
protected static ?float $c = 5.0;
private static ?self $d;
public readonly int|float $e;
}
-----
!!php7
@ -17,4 +18,5 @@ class A
public string $b;
protected static ?float $c = 5.0;
private static ?self $d;
public readonly int|float $e;
}