Skip to content

Commit

Permalink
[PHP 8.2] Add readonly class support (#834)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed May 15, 2022
1 parent 5d83adc commit 678ccbe
Show file tree
Hide file tree
Showing 12 changed files with 1,130 additions and 929 deletions.
14 changes: 12 additions & 2 deletions grammar/php7.y
Expand Up @@ -382,8 +382,18 @@ enum_case_expr:

class_entry_type:
T_CLASS { $$ = 0; }
| T_ABSTRACT T_CLASS { $$ = Stmt\Class_::MODIFIER_ABSTRACT; }
| T_FINAL T_CLASS { $$ = Stmt\Class_::MODIFIER_FINAL; }
| class_modifiers T_CLASS { $$ = $1; }
;

class_modifiers:
class_modifier { $$ = $1; }
| class_modifiers class_modifier { $this->checkClassModifier($1, $2, #2); $$ = $1 | $2; }
;

class_modifier:
T_ABSTRACT { $$ = Stmt\Class_::MODIFIER_ABSTRACT; }
| T_FINAL { $$ = Stmt\Class_::MODIFIER_FINAL; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
;

extends_from:
Expand Down
10 changes: 8 additions & 2 deletions lib/PhpParser/Builder/Class_.php
Expand Up @@ -67,7 +67,7 @@ public function implement(...$interfaces) {
* @return $this The builder instance (for fluid interface)
*/
public function makeAbstract() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_ABSTRACT);
$this->flags = BuilderHelpers::addClassModifier($this->flags, Stmt\Class_::MODIFIER_ABSTRACT);

return $this;
}
Expand All @@ -78,7 +78,13 @@ public function makeAbstract() {
* @return $this The builder instance (for fluid interface)
*/
public function makeFinal() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_FINAL);
$this->flags = BuilderHelpers::addClassModifier($this->flags, Stmt\Class_::MODIFIER_FINAL);

return $this;
}

public function makeReadonly() {
$this->flags = BuilderHelpers::addClassModifier($this->flags, Stmt\Class_::MODIFIER_READONLY);

return $this;
}
Expand Down
9 changes: 9 additions & 0 deletions lib/PhpParser/BuilderHelpers.php
Expand Up @@ -310,4 +310,13 @@ public static function addModifier(int $modifiers, int $modifier) : int {
Stmt\Class_::verifyModifier($modifiers, $modifier);
return $modifiers | $modifier;
}

/**
* Adds a modifier and returns new modifier bitmask.
* @return int New modifiers
*/
public static function addClassModifier(int $existingModifiers, int $modifierToSet) : int {
Stmt\Class_::verifyClassModifier($existingModifiers, $modifierToSet);
return $existingModifiers | $modifierToSet;
}
}
25 changes: 25 additions & 0 deletions lib/PhpParser/Node/Stmt/Class_.php
Expand Up @@ -68,6 +68,10 @@ public function isFinal() : bool {
return (bool) ($this->flags & self::MODIFIER_FINAL);
}

public function isReadonly() : bool {
return (bool) ($this->flags & self::MODIFIER_READONLY);
}

/**
* Whether the class is anonymous.
*
Expand All @@ -77,6 +81,27 @@ public function isAnonymous() : bool {
return null === $this->name;
}

/**
* @internal
*/
public static function verifyClassModifier($a, $b) {
if ($a & self::MODIFIER_ABSTRACT && $b & self::MODIFIER_ABSTRACT) {
throw new Error('Multiple abstract modifiers are not allowed');
}

if ($a & self::MODIFIER_FINAL && $b & self::MODIFIER_FINAL) {
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');
}
}

/**
* @internal
*/
Expand Down
1,862 changes: 944 additions & 918 deletions lib/PhpParser/Parser/Php7.php

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions lib/PhpParser/ParserAbstract.php
Expand Up @@ -875,6 +875,15 @@ protected function createCommentNopAttributes(array $comments) {
return $attributes;
}

protected function checkClassModifier($a, $b, $modifierPos) {
try {
Class_::verifyClassModifier($a, $b);
} catch (Error $error) {
$error->setAttributes($this->getAttributesAt($modifierPos));
$this->emitError($error);
}
}

protected function checkModifier($a, $b, $modifierPos) {
// Jumping through some hoops here because verifyModifier() is also used elsewhere
try {
Expand Down
14 changes: 14 additions & 0 deletions test/PhpParser/Builder/ClassTest.php
Expand Up @@ -68,6 +68,20 @@ public function testFinal() {
);
}

public function testReadonly() {
$node = $this->createClassBuilder('Test')
->makeReadonly()
->getNode()
;

$this->assertEquals(
new Stmt\Class_('Test', [
'flags' => Stmt\Class_::MODIFIER_READONLY
]),
$node
);
}

public function testStatementOrder() {
$method = new Stmt\ClassMethod('testMethod');
$property = new Stmt\Property(
Expand Down
2 changes: 1 addition & 1 deletion test/code/parser/errorHandling/recovery.test
Expand Up @@ -1521,4 +1521,4 @@ array(
)
)
)
)
)
2 changes: 1 addition & 1 deletion test/code/parser/stmt/class/constModifierErrors.test
Expand Up @@ -150,4 +150,4 @@ array(
)
)
)
)
)
32 changes: 27 additions & 5 deletions test/code/parser/stmt/class/modifier.test
Expand Up @@ -66,7 +66,7 @@ array(
)
)
-----
<?php class A { readonly readonly $a; }
<?php class C { readonly readonly $a; }
-----
!!php7
Multiple readonly modifiers are not allowed from 1:26 to 1:33
Expand All @@ -76,7 +76,7 @@ array(
)
flags: 0
name: Identifier(
name: A
name: C
)
extends: null
implements: array(
Expand Down Expand Up @@ -231,8 +231,29 @@ array(
)
-----
<?php abstract final class A { }
-----
!!php7
Cannot use the final modifier on an abstract class from 1:16 to 1:20
array(
0: Stmt_Class(
attrGroups: array(
)
flags: MODIFIER_ABSTRACT | MODIFIER_FINAL (48)
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
)
)
)
-----
<?php abstract final class A { }
// Type in the partial parse could conceivably be any of 0, 16 or 32
-----
!!php5
Syntax error, unexpected T_FINAL, expecting T_CLASS from 1:16 to 1:20
array(
0: Stmt_Class(
Expand All @@ -258,6 +279,7 @@ array(
<?php readonly class A { }
// Type in the partial parse could conceivably be any of 0, 16 or 32
-----
!!php5
Syntax error, unexpected T_READONLY from 1:7 to 1:14
array(
0: Stmt_Class(
Expand All @@ -280,7 +302,7 @@ array(
)
)
-----
<?php class A { abstract $a; }
<?php class B { abstract $b; }
-----
Properties cannot be declared abstract from 1:17 to 1:24
array(
Expand All @@ -289,7 +311,7 @@ array(
)
flags: 0
name: Identifier(
name: A
name: B
)
extends: null
implements: array(
Expand All @@ -303,7 +325,7 @@ array(
props: array(
0: Stmt_PropertyProperty(
name: VarLikeIdentifier(
name: a
name: b
)
default: null
)
Expand Down
68 changes: 68 additions & 0 deletions test/code/parser/stmt/class/readonly.test
@@ -0,0 +1,68 @@
Readonly class
-----
<?php

readonly class A {
}
-----
!!php7
array(
0: Stmt_Class(
attrGroups: array(
)
flags: MODIFIER_READONLY (64)
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
)
)
)
-----
<?php

readonly class A {
}
-----
!!php5
Syntax error, unexpected T_READONLY from 3:1 to 3:8
array(
0: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
)
)
)
-----
<?php

final readonly class A {
}
-----
!!php7
array(
0: Stmt_Class(
attrGroups: array(
)
flags: MODIFIER_FINAL | MODIFIER_READONLY (96)
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
)
)
)
12 changes: 12 additions & 0 deletions test/code/prettyPrinter/stmt/readonly_class.test
@@ -0,0 +1,12 @@
Readonly class
-----
<?php

readonly class Foo
{
}
-----
!!php7
readonly class Foo
{
}

0 comments on commit 678ccbe

Please sign in to comment.