Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PHP 8.2] Add readonly class support #834

Merged
merged 7 commits into from May 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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');
}
}
nikic marked this conversation as resolved.
Show resolved Hide resolved

/**
* @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; }
nikic marked this conversation as resolved.
Show resolved Hide resolved
-----
!!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
{
}