Skip to content

Commit 9d2e500

Browse files
authored
[PHP 8.2] Add readonly class support (#834)
RFC: https://wiki.php.net/rfc/readonly_classes PHP implementation: php/php-src#7305
1 parent 0abe993 commit 9d2e500

File tree

12 files changed

+1130
-929
lines changed

12 files changed

+1130
-929
lines changed

grammar/php7.y

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,18 @@ enum_case_expr:
382382

383383
class_entry_type:
384384
T_CLASS { $$ = 0; }
385-
| T_ABSTRACT T_CLASS { $$ = Stmt\Class_::MODIFIER_ABSTRACT; }
386-
| T_FINAL T_CLASS { $$ = Stmt\Class_::MODIFIER_FINAL; }
385+
| class_modifiers T_CLASS { $$ = $1; }
386+
;
387+
388+
class_modifiers:
389+
class_modifier { $$ = $1; }
390+
| class_modifiers class_modifier { $this->checkClassModifier($1, $2, #2); $$ = $1 | $2; }
391+
;
392+
393+
class_modifier:
394+
T_ABSTRACT { $$ = Stmt\Class_::MODIFIER_ABSTRACT; }
395+
| T_FINAL { $$ = Stmt\Class_::MODIFIER_FINAL; }
396+
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
387397
;
388398

389399
extends_from:

lib/PhpParser/Builder/Class_.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function implement(...$interfaces) {
6767
* @return $this The builder instance (for fluid interface)
6868
*/
6969
public function makeAbstract() {
70-
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_ABSTRACT);
70+
$this->flags = BuilderHelpers::addClassModifier($this->flags, Stmt\Class_::MODIFIER_ABSTRACT);
7171

7272
return $this;
7373
}
@@ -78,7 +78,13 @@ public function makeAbstract() {
7878
* @return $this The builder instance (for fluid interface)
7979
*/
8080
public function makeFinal() {
81-
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_FINAL);
81+
$this->flags = BuilderHelpers::addClassModifier($this->flags, Stmt\Class_::MODIFIER_FINAL);
82+
83+
return $this;
84+
}
85+
86+
public function makeReadonly() {
87+
$this->flags = BuilderHelpers::addClassModifier($this->flags, Stmt\Class_::MODIFIER_READONLY);
8288

8389
return $this;
8490
}

lib/PhpParser/BuilderHelpers.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,13 @@ public static function addModifier(int $modifiers, int $modifier) : int {
310310
Stmt\Class_::verifyModifier($modifiers, $modifier);
311311
return $modifiers | $modifier;
312312
}
313+
314+
/**
315+
* Adds a modifier and returns new modifier bitmask.
316+
* @return int New modifiers
317+
*/
318+
public static function addClassModifier(int $existingModifiers, int $modifierToSet) : int {
319+
Stmt\Class_::verifyClassModifier($existingModifiers, $modifierToSet);
320+
return $existingModifiers | $modifierToSet;
321+
}
313322
}

lib/PhpParser/Node/Stmt/Class_.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ public function isFinal() : bool {
6868
return (bool) ($this->flags & self::MODIFIER_FINAL);
6969
}
7070

71+
public function isReadonly() : bool {
72+
return (bool) ($this->flags & self::MODIFIER_READONLY);
73+
}
74+
7175
/**
7276
* Whether the class is anonymous.
7377
*
@@ -77,6 +81,27 @@ public function isAnonymous() : bool {
7781
return null === $this->name;
7882
}
7983

84+
/**
85+
* @internal
86+
*/
87+
public static function verifyClassModifier($a, $b) {
88+
if ($a & self::MODIFIER_ABSTRACT && $b & self::MODIFIER_ABSTRACT) {
89+
throw new Error('Multiple abstract modifiers are not allowed');
90+
}
91+
92+
if ($a & self::MODIFIER_FINAL && $b & self::MODIFIER_FINAL) {
93+
throw new Error('Multiple final modifiers are not allowed');
94+
}
95+
96+
if ($a & self::MODIFIER_READONLY && $b & self::MODIFIER_READONLY) {
97+
throw new Error('Multiple readonly modifiers are not allowed');
98+
}
99+
100+
if ($a & 48 && $b & 48) {
101+
throw new Error('Cannot use the final modifier on an abstract class');
102+
}
103+
}
104+
80105
/**
81106
* @internal
82107
*/

lib/PhpParser/Parser/Php7.php

Lines changed: 944 additions & 918 deletions
Large diffs are not rendered by default.

lib/PhpParser/ParserAbstract.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,15 @@ protected function createCommentNopAttributes(array $comments) {
875875
return $attributes;
876876
}
877877

878+
protected function checkClassModifier($a, $b, $modifierPos) {
879+
try {
880+
Class_::verifyClassModifier($a, $b);
881+
} catch (Error $error) {
882+
$error->setAttributes($this->getAttributesAt($modifierPos));
883+
$this->emitError($error);
884+
}
885+
}
886+
878887
protected function checkModifier($a, $b, $modifierPos) {
879888
// Jumping through some hoops here because verifyModifier() is also used elsewhere
880889
try {

test/PhpParser/Builder/ClassTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ public function testFinal() {
6868
);
6969
}
7070

71+
public function testReadonly() {
72+
$node = $this->createClassBuilder('Test')
73+
->makeReadonly()
74+
->getNode()
75+
;
76+
77+
$this->assertEquals(
78+
new Stmt\Class_('Test', [
79+
'flags' => Stmt\Class_::MODIFIER_READONLY
80+
]),
81+
$node
82+
);
83+
}
84+
7185
public function testStatementOrder() {
7286
$method = new Stmt\ClassMethod('testMethod');
7387
$property = new Stmt\Property(

test/code/parser/errorHandling/recovery.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1521,4 +1521,4 @@ array(
15211521
)
15221522
)
15231523
)
1524-
)
1524+
)

test/code/parser/stmt/class/constModifierErrors.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,4 @@ array(
150150
)
151151
)
152152
)
153-
)
153+
)

test/code/parser/stmt/class/modifier.test

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ array(
6666
)
6767
)
6868
-----
69-
<?php class A { readonly readonly $a; }
69+
<?php class C { readonly readonly $a; }
7070
-----
7171
!!php7
7272
Multiple readonly modifiers are not allowed from 1:26 to 1:33
@@ -76,7 +76,7 @@ array(
7676
)
7777
flags: 0
7878
name: Identifier(
79-
name: A
79+
name: C
8080
)
8181
extends: null
8282
implements: array(
@@ -231,8 +231,29 @@ array(
231231
)
232232
-----
233233
<?php abstract final class A { }
234+
-----
235+
!!php7
236+
Cannot use the final modifier on an abstract class from 1:16 to 1:20
237+
array(
238+
0: Stmt_Class(
239+
attrGroups: array(
240+
)
241+
flags: MODIFIER_ABSTRACT | MODIFIER_FINAL (48)
242+
name: Identifier(
243+
name: A
244+
)
245+
extends: null
246+
implements: array(
247+
)
248+
stmts: array(
249+
)
250+
)
251+
)
252+
-----
253+
<?php abstract final class A { }
234254
// Type in the partial parse could conceivably be any of 0, 16 or 32
235255
-----
256+
!!php5
236257
Syntax error, unexpected T_FINAL, expecting T_CLASS from 1:16 to 1:20
237258
array(
238259
0: Stmt_Class(
@@ -258,6 +279,7 @@ array(
258279
<?php readonly class A { }
259280
// Type in the partial parse could conceivably be any of 0, 16 or 32
260281
-----
282+
!!php5
261283
Syntax error, unexpected T_READONLY from 1:7 to 1:14
262284
array(
263285
0: Stmt_Class(
@@ -280,7 +302,7 @@ array(
280302
)
281303
)
282304
-----
283-
<?php class A { abstract $a; }
305+
<?php class B { abstract $b; }
284306
-----
285307
Properties cannot be declared abstract from 1:17 to 1:24
286308
array(
@@ -289,7 +311,7 @@ array(
289311
)
290312
flags: 0
291313
name: Identifier(
292-
name: A
314+
name: B
293315
)
294316
extends: null
295317
implements: array(
@@ -303,7 +325,7 @@ array(
303325
props: array(
304326
0: Stmt_PropertyProperty(
305327
name: VarLikeIdentifier(
306-
name: a
328+
name: b
307329
)
308330
default: null
309331
)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Readonly class
2+
-----
3+
<?php
4+
5+
readonly class A {
6+
}
7+
-----
8+
!!php7
9+
array(
10+
0: Stmt_Class(
11+
attrGroups: array(
12+
)
13+
flags: MODIFIER_READONLY (64)
14+
name: Identifier(
15+
name: A
16+
)
17+
extends: null
18+
implements: array(
19+
)
20+
stmts: array(
21+
)
22+
)
23+
)
24+
-----
25+
<?php
26+
27+
readonly class A {
28+
}
29+
-----
30+
!!php5
31+
Syntax error, unexpected T_READONLY from 3:1 to 3:8
32+
array(
33+
0: Stmt_Class(
34+
attrGroups: array(
35+
)
36+
flags: 0
37+
name: Identifier(
38+
name: A
39+
)
40+
extends: null
41+
implements: array(
42+
)
43+
stmts: array(
44+
)
45+
)
46+
)
47+
-----
48+
<?php
49+
50+
final readonly class A {
51+
}
52+
-----
53+
!!php7
54+
array(
55+
0: Stmt_Class(
56+
attrGroups: array(
57+
)
58+
flags: MODIFIER_FINAL | MODIFIER_READONLY (96)
59+
name: Identifier(
60+
name: A
61+
)
62+
extends: null
63+
implements: array(
64+
)
65+
stmts: array(
66+
)
67+
)
68+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Readonly class
2+
-----
3+
<?php
4+
5+
readonly class Foo
6+
{
7+
}
8+
-----
9+
!!php7
10+
readonly class Foo
11+
{
12+
}

0 commit comments

Comments
 (0)