Skip to content

Commit dcfe2ae

Browse files
Francesco Zappa Nardellimeta-codesync[bot]
authored andcommitted
Sealed methods
Summary: HIP for sealed methods Reviewed By: andrewjkennedy Differential Revision: D86402411 fbshipit-source-id: aaa71e09629964765746360e5bfa9ee8b516331c
1 parent 50ce82e commit dcfe2ae

1 file changed

Lines changed: 316 additions & 0 deletions

File tree

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
## [HIP] `__Sealed` methods
2+
3+
**First proposal**: 22/4/2024
4+
5+
**Updated**: 16/10/2025
6+
7+
**Author**: Francesco Zappa Nardelli
8+
9+
**Contributors**: Adi Kumar, Catherine Gasnier, Mistral Contrastin, Jake Cordero, Scott Owens, Shashank Kambhampati.
10+
11+
**TL;DR** Following feedback from programmers it seems that there is interest in having a method-level `__Sealed` attribute that controls which subclasses can override the method. This document proposes a design, a prototype implementation is available for experimentation.
12+
13+
**Description**: we propose to add a method-level `__Sealed` attribute. Semantics is defined below, according to the placement of the attribute.
14+
15+
1. **Sealed method defined in a concrete class.**
16+
17+
If the attribute is added to a method defined in a **concrete class**, the attribute controls which subclasses can override the method. This part of the design is straightforward and is exemplified by the following examples. The simplest use-case is below, which would be **accepted** since `foo` is sealed with class `D`, and overridden in class `D`.
18+
19+
```php
20+
class C {
21+
<<__Sealed(D::class)>>
22+
public function foo(): void { echo "I am foo in C\n"; }
23+
}
24+
25+
class D extends C {
26+
<<__Override>>
27+
public function foo(): void { echo "I am foo in D\n"; }
28+
}
29+
```
30+
31+
On the other hand, continuing the example, class E would be **rejected** as it is not allowed to override `foo` in `C`.
32+
33+
```php
34+
...
35+
36+
class E extends C {
37+
<<__Override>>
38+
public function foo(): void { echo "I am foo in E\n"; }
39+
}
40+
```
41+
42+
Class-level `__Sealed` attributes only impose restrictions on direct extends, so a class that extends `D` can override `foo` again. We mimic the same semantics with method-level `__Sealed` attributes, so building on our running example, the following is accepted:
43+
44+
```php
45+
...
46+
47+
class F extends D {
48+
<<__Override>>
49+
public function foo(): void { echo "I am foo in F which extends D\n"; }
50+
}
51+
```
52+
53+
Similarly, it is possible to introduce new `__Sealed` restrictions in subclasses, eg. the following is accepted:
54+
55+
```php
56+
class C {
57+
<<__Sealed(D::class)>>
58+
public function foo(): void { echo "I am foo in C\n"; }
59+
}
60+
61+
class D extends C {
62+
<<__Override, __Sealed(E::class)>>
63+
public function foo(): void { echo "I am foo in D\n"; }
64+
}
65+
66+
class E extends D {
67+
<<__Override>>
68+
public function foo(): void { echo "I am foo in E\n"; }
69+
}
70+
```
71+
72+
It is arguable whether the following should be accepted or rejected, since there is no analogous scenario in class-level `__Sealed` attributes:
73+
74+
```php
75+
class C {
76+
<<__Sealed(D::class)>>
77+
public function foo(): void { echo "I am foo in C\n"; }
78+
}
79+
80+
class D extends C {}
81+
82+
class E extends D {
83+
<<__Override>>
84+
public function foo(): void { echo "I am foo in E\n"; }
85+
}
86+
```
87+
88+
We propose to reject this code so that the `__Sealed(D::class)` attribute in C can be interpreted as _"if foo in C is overridden, then it is overridden in D"_.
89+
90+
2. **Sealed method defined in an abstract class.**
91+
92+
If the attribute is added to an abstract method defined in an **abstract class**, then we have two options (and we should support both):
93+
94+
* the `__Sealed` attribute can specify which **classes** can implement the abstract method, as in the class `D` below:
95+
96+
```php
97+
abstract class C {
98+
<<__Sealed(D::class)>>
99+
abstract public function foo(): void;
100+
}
101+
102+
// The __Sealed attribute can specify the class
103+
// that implements the abstract method
104+
class D extends C {
105+
<<__Override>>
106+
public function foo(): void { echo "I am foo in D\n"; }
107+
}
108+
```
109+
110+
The abstract class `C` might also defer the overriding implementation of `foo` to a trait, as below.
111+
112+
```php
113+
// method_abstractclass_02.php
114+
115+
abstract class C {
116+
<<__Sealed(D::class)>>
117+
abstract public function foo(): void;
118+
}
119+
120+
trait T {
121+
<<__Override>>
122+
public function foo(): void { echo "I am foo in T\n"; }
123+
}
124+
125+
class D extends C {
126+
use T;
127+
}
128+
```
129+
130+
* the `__Sealed` attribute can specify which **traits** can implement the abstract method. Arbitrary classes can then use the traits, as in the valid code below:
131+
132+
```php
133+
// method_abstractclass_03.php
134+
135+
abstract class C {
136+
<<__Sealed(T::class)>>
137+
abstract public function foo(): void;
138+
}
139+
140+
// The __Sealed attribute can specify the trait
141+
// that implements the abstract method.
142+
trait T {
143+
public function foo(): void { echo "I am foo in T\n"; }
144+
}
145+
146+
// Arbitrary subclasses of C can then use the trait.
147+
class E extends C {
148+
use T;
149+
}
150+
151+
class F extends C {
152+
use T;
153+
}
154+
```
155+
156+
If a method is sealed with a trait, then it is not possible to redefine the trait method in the class that uses the trait (otherwise the `__Sealed(T::class)` annotation would be useless). The following is thus rejected:
157+
158+
```php
159+
// method_abstractclass_04.php
160+
161+
abstract class C {
162+
<<__Sealed(T::class)>>
163+
abstract public function foo(): void;
164+
}
165+
166+
trait T {
167+
<<__Override>>
168+
public function foo(): void { echo "I am foo in T\n"; }
169+
}
170+
171+
class D extends C {
172+
use T;
173+
// This should be rejected
174+
<<__Override>>
175+
public function foo(): void { echo "I am foo in D\n"; }
176+
}
177+
178+
class E extends C {
179+
// This should be rejected
180+
<<__Override>>
181+
public function foo(): void { echo "I am foo in E2\n"; }
182+
}
183+
184+
```
185+
186+
If a method is sealed with a trait, then it is not possible to redefine the trait method in the class that uses the trait (otherwise the `__Sealed(T::class)` annotation would be useless). The following is thus rejected:
187+
188+
```php
189+
// method_abstractclass_05.php
190+
191+
abstract class C {
192+
<<__Sealed(T1::class)>>
193+
abstract public function foo(): void;
194+
}
195+
196+
trait T1 {
197+
<<__Override>>
198+
public function foo(): void { echo "I am foo in T1\n"; }
199+
}
200+
201+
trait T2 {
202+
<<__Override>>
203+
public function foo(): void { echo "I am foo in T2\n"; }
204+
}
205+
206+
class D1 extends C {
207+
use T1; // this is ok
208+
}
209+
210+
class D2 extends C {
211+
use T2; // this is rejected
212+
}
213+
```
214+
215+
In both cases the rules for transitive visibility of `__Sealed` attributes for methods defined in abstract classes and traits are similar to those for methods defined in classes (eg. no transitive interpretation).
216+
217+
3. **Sealed method defined in a trait**
218+
219+
If the attribute is added to methods defined in a **trait**, then again, the `__Sealed` attribute can specify which classes or which traits can override the method. The following is thus accepted:
220+
221+
```php
222+
// method_trait_01.php
223+
224+
trait T1 {
225+
<<__Sealed(C::class, T2::class)>>
226+
public function foo(): void { echo "I am foo in T1\n"; }
227+
}
228+
229+
// class C can override foo in T1
230+
class C {
231+
use T1;
232+
233+
<<__Override>>
234+
public function foo(): void { echo "I am foo in C\n"; }
235+
}
236+
237+
// trait T2 can override foo in T1, T2 can then be used by arbitrary classes
238+
trait T2 {
239+
use T1;
240+
241+
<<__Override>>
242+
public function foo(): void { echo "I am foo in T2\n"; }
243+
}
244+
245+
class D {
246+
use T2;
247+
}
248+
```
249+
250+
while the following overrides are rejected:
251+
252+
```php
253+
// method_trait_02.php
254+
255+
trait T1 {
256+
<<__Sealed(C::class, T2::class)>>
257+
public function foo(): void { echo "I am foo in T1\n"; }
258+
}
259+
260+
// class D cannot override foo from T1
261+
class D {
262+
use T1;
263+
264+
<<__Override>>
265+
public function foo(): void { echo "I am foo in D\n"; }
266+
}
267+
268+
// trait T3 cannot override foo from T1
269+
trait T3 {
270+
use T1;
271+
272+
<<__Override>>
273+
public function foo(): void { echo "I am foo in T3\n"; }
274+
}
275+
```
276+
277+
The following is rejected too (the rule of thumb is _"if T1 is inlined in E1 then E2 cannot override foo"_):
278+
279+
```php
280+
// method_trait_03.php
281+
282+
trait T1 {
283+
<<__Sealed(C::class, T2::class)>>
284+
public function foo(): void { echo "I am foo in T1\n"; }
285+
}
286+
287+
class E1 {
288+
use T1;
289+
}
290+
291+
class E2 extends E1 {
292+
<<__Override>>
293+
// this is rejected
294+
public function foo(): void { echo "I am foo in E2\n"; }
295+
}
296+
297+
```
298+
299+
**Syntactic restrictions:**
300+
301+
- Sealed cannot be used on constructors and private methods
302+
- Sealed cannot be used on methods defined in interfaces
303+
- Methods cannot be sealed to names that are not visible (eg. `internal` to a different module)
304+
305+
**Property:**
306+
307+
If a method is sealed with an empty list of classes, then it cannot be overridden (eg. it is final).
308+
309+
**Implementation**:
310+
311+
**Hack**: we have a prototype implementation that can be used for experimentation:
312+
* allow `__Sealed` on methods,
313+
* store in shallow decls
314+
* while doing override checks, enforce the sealed semantics whenever the overridden method has the relevant attribute
315+
316+
**HHVM**: HHVM enforces the class-level Sealed attributes at class/trait load time. We argue that HHVM does not need to enforce method-level `__Sealed` attributes. If needed, we can mimic the enforcement for method-level Sealed attributes. No extra run-time checks / overhead are needed, and no changes to Func objects are required.

0 commit comments

Comments
 (0)