From 728e39d81f86749c5d31ed0df38d803aed46331c Mon Sep 17 00:00:00 2001 From: klimick Date: Sat, 12 Sep 2026 15:27:48 +0300 Subject: [PATCH 1/5] Validate @phpstan-sealed subtypes --- .../PhpDoc/SealedDefinitionClassRule.php | 12 ++++ .../PhpDoc/SealedDefinitionClassRuleTest.php | 39 +++++++++++ .../Rules/PhpDoc/data/sealed-subtypes.php | 67 +++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php diff --git a/src/Rules/PhpDoc/SealedDefinitionClassRule.php b/src/Rules/PhpDoc/SealedDefinitionClassRule.php index a00a6b2f64d..8dd7f35efe5 100644 --- a/src/Rules/PhpDoc/SealedDefinitionClassRule.php +++ b/src/Rules/PhpDoc/SealedDefinitionClassRule.php @@ -84,6 +84,18 @@ public function processNode(Node $node, Scope $scope): array continue; } + if (!$this->reflectionProvider->getClass($class)->is($classReflection->getName())) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @phpstan-sealed type %s is not subtype of %s.', + $class, + $classReflection->getName(), + ))->identifier('sealed.notSubtype'); + + $errors[] = $errorBuilder->build(); + + continue; + } + $errors = array_merge( $errors, $this->classCheck->checkClassNames($scope, [ diff --git a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php index 92a409fd914..6b9e34ae605 100644 --- a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php @@ -53,11 +53,50 @@ public function testRule(): void 26, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], + [ + 'PHPDoc tag @phpstan-sealed type IncompatibleSealed\SomeClass is not subtype of IncompatibleSealed\Valid.', + 31, + ], + [ + 'PHPDoc tag @phpstan-sealed type IncompatibleSealed\SomeClass is not subtype of IncompatibleSealed\ValidInterface.', + 36, + ], + [ + 'PHPDoc tag @phpstan-sealed type IncompatibleSealed\SomeInterface is not subtype of IncompatibleSealed\ValidInterface2.', + 41, + ], [ 'PHPDoc tag @phpstan-sealed contains unknown class IncompatibleSealed\UnknownClass.', 46, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], + [ + 'PHPDoc tag @phpstan-sealed type IncompatibleSealed\SomeClass is not subtype of IncompatibleSealed\InvalidClassWithUnion.', + 46, + ], + ]); + } + + #[RequiresPhp('>= 8.2.0')] + public function testSubtypes(): void + { + $this->analyse([__DIR__ . '/data/sealed-subtypes.php'], [ + [ + 'PHPDoc tag @phpstan-sealed type SealedSubtypes\\__YEnumInvalid is not subtype of SealedSubtypes\\__EnumError.', + 23, + ], + [ + 'PHPDoc tag @phpstan-sealed type SealedSubtypes\\__YInterfaceInvalid is not subtype of SealedSubtypes\\__InterfaceError.', + 37, + ], + [ + 'PHPDoc tag @phpstan-sealed type SealedSubtypes\\__YAbstractClassInvalid is not subtype of SealedSubtypes\\__AbstractClassError.', + 51, + ], + [ + 'PHPDoc tag @phpstan-sealed type SealedSubtypes\\__YClassInvalid is not subtype of SealedSubtypes\\__ClassError.', + 65, + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php b/tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php new file mode 100644 index 00000000000..840a5ef5ea8 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php @@ -0,0 +1,67 @@ += 8.2 + +namespace SealedSubtypes; + +/** + * Subtyping is reflexive, so Reflexivity is a subtype of itself. + * This is a degenerate sealed declaration, but it is valid and must not report an error. + * + * @phpstan-sealed Reflexivity + */ +interface Reflexivity {} + +/** @phpstan-sealed _XEnum */ +interface _Enum {} +enum _XEnum implements _Enum { case Value; } + +/** @phpstan-sealed __XEnum | __YEnum */ +interface __Enum {} +enum __XEnum implements __Enum { case Value; } +enum __YEnum implements __Enum { case Value; } + +/** @phpstan-sealed __XEnumValid | __YEnumInvalid */ +interface __EnumError {} +enum __XEnumValid implements __EnumError { case Value; } +enum __YEnumInvalid { case Value; } + +/** @phpstan-sealed _XInterface */ +interface _Interface {} +interface _XInterface extends _Interface {} + +/** @phpstan-sealed __XInterface | __YInterface */ +interface __Interface {} +interface __XInterface extends __Interface {} +interface __YInterface extends __Interface {} + +/** @phpstan-sealed __XInterfaceValid | __YInterfaceInvalid */ +interface __InterfaceError {} +interface __XInterfaceValid extends __InterfaceError {} +interface __YInterfaceInvalid {} + +/** @phpstan-sealed _XAbstractClass */ +abstract readonly class _AbstractClass {} +abstract readonly class _XAbstractClass extends _AbstractClass {} + +/** @phpstan-sealed __XAbstractClass | __YAbstractClass */ +abstract readonly class __AbstractClass {} +abstract readonly class __XAbstractClass extends __AbstractClass {} +abstract readonly class __YAbstractClass extends __AbstractClass {} + +/** @phpstan-sealed __XAbstractClassValid | __YAbstractClassInvalid */ +abstract readonly class __AbstractClassError {} +abstract readonly class __XAbstractClassValid extends __AbstractClassError {} +abstract readonly class __YAbstractClassInvalid {} + +/** @phpstan-sealed _XClass */ +abstract readonly class _Class {} +final readonly class _XClass extends _Class {} + +/** @phpstan-sealed __XClass | __YClass */ +abstract readonly class __Class {} +final readonly class __XClass extends __Class {} +final readonly class __YClass extends __Class {} + +/** @phpstan-sealed __XClassValid | __YClassInvalid */ +abstract readonly class __ClassError {} +final readonly class __XClassValid extends __ClassError {} +final readonly class __YClassInvalid {} From c881b0aef19964dd8b07f960d76c41357c4ce853 Mon Sep 17 00:00:00 2001 From: klimick Date: Sat, 12 Sep 2026 20:01:54 +0300 Subject: [PATCH 2/5] Allow open branches in @phpstan-sealed types --- .../PhpDoc/SealedDefinitionClassRule.php | 9 ++++- .../PhpDoc/SealedDefinitionClassRuleTest.php | 37 ++++++------------- .../PhpDoc/data/sealed-non-final-subtypes.php | 22 +++++++++++ 3 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 tests/PHPStan/Rules/PhpDoc/data/sealed-non-final-subtypes.php diff --git a/src/Rules/PhpDoc/SealedDefinitionClassRule.php b/src/Rules/PhpDoc/SealedDefinitionClassRule.php index 8dd7f35efe5..a324f3afce4 100644 --- a/src/Rules/PhpDoc/SealedDefinitionClassRule.php +++ b/src/Rules/PhpDoc/SealedDefinitionClassRule.php @@ -84,9 +84,14 @@ public function processNode(Node $node, Scope $scope): array continue; } - if (!$this->reflectionProvider->getClass($class)->is($classReflection->getName())) { + $sealedTypeReflection = $this->reflectionProvider->getClass($class); + + if ( + ($sealedTypeReflection->isEnum() || $sealedTypeReflection->isFinal()) + && !$sealedTypeReflection->is($classReflection->getName()) + ) { $errorBuilder = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @phpstan-sealed type %s is not subtype of %s.', + 'PHPDoc tag @phpstan-sealed contains final type %s that is not subtype of %s.', $class, $classReflection->getName(), ))->identifier('sealed.notSubtype'); diff --git a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php index 6b9e34ae605..ea2972097c1 100644 --- a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php @@ -53,27 +53,11 @@ public function testRule(): void 26, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], - [ - 'PHPDoc tag @phpstan-sealed type IncompatibleSealed\SomeClass is not subtype of IncompatibleSealed\Valid.', - 31, - ], - [ - 'PHPDoc tag @phpstan-sealed type IncompatibleSealed\SomeClass is not subtype of IncompatibleSealed\ValidInterface.', - 36, - ], - [ - 'PHPDoc tag @phpstan-sealed type IncompatibleSealed\SomeInterface is not subtype of IncompatibleSealed\ValidInterface2.', - 41, - ], [ 'PHPDoc tag @phpstan-sealed contains unknown class IncompatibleSealed\UnknownClass.', 46, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], - [ - 'PHPDoc tag @phpstan-sealed type IncompatibleSealed\SomeClass is not subtype of IncompatibleSealed\InvalidClassWithUnion.', - 46, - ], ]); } @@ -82,20 +66,23 @@ public function testSubtypes(): void { $this->analyse([__DIR__ . '/data/sealed-subtypes.php'], [ [ - 'PHPDoc tag @phpstan-sealed type SealedSubtypes\\__YEnumInvalid is not subtype of SealedSubtypes\\__EnumError.', + 'PHPDoc tag @phpstan-sealed contains final type SealedSubtypes\\__YEnumInvalid that is not subtype of SealedSubtypes\\__EnumError.', 23, ], [ - 'PHPDoc tag @phpstan-sealed type SealedSubtypes\\__YInterfaceInvalid is not subtype of SealedSubtypes\\__InterfaceError.', - 37, - ], - [ - 'PHPDoc tag @phpstan-sealed type SealedSubtypes\\__YAbstractClassInvalid is not subtype of SealedSubtypes\\__AbstractClassError.', - 51, + 'PHPDoc tag @phpstan-sealed contains final type SealedSubtypes\\__YClassInvalid that is not subtype of SealedSubtypes\\__ClassError.', + 65, ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testNonFinalSubtypes(): void + { + $this->analyse([__DIR__ . '/data/sealed-non-final-subtypes.php'], [ [ - 'PHPDoc tag @phpstan-sealed type SealedSubtypes\\__YClassInvalid is not subtype of SealedSubtypes\\__ClassError.', - 65, + 'PHPDoc tag @phpstan-sealed contains final type SealedNonFinalSubtypes\\InvalidZ that is not subtype of SealedNonFinalSubtypes\\InvalidSealed.', + 8, ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/data/sealed-non-final-subtypes.php b/tests/PHPStan/Rules/PhpDoc/data/sealed-non-final-subtypes.php new file mode 100644 index 00000000000..76204fa16e9 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/sealed-non-final-subtypes.php @@ -0,0 +1,22 @@ += 8.1 + +namespace SealedNonFinalSubtypes; + +/** + * @phpstan-sealed InvalidX|InvalidY|InvalidZ + */ +interface InvalidSealed {} + +final class InvalidX implements InvalidSealed {} +final class InvalidY implements InvalidSealed {} +final class InvalidZ {} + +/** + * @phpstan-sealed ValidX|ValidY|ValidZ + */ +interface ValidSealed {} + +final class ValidX implements ValidSealed {} +final class ValidY implements ValidSealed {} +class ValidZ {} +class ValidZZ extends ValidZ {} From dfe06f45668387f48da7daa5c64cf854c8fd979a Mon Sep 17 00:00:00 2001 From: klimick Date: Sat, 12 Sep 2026 20:02:13 +0300 Subject: [PATCH 3/5] Enable sealed subtype validation in bleeding edge --- src/Rules/PhpDoc/SealedDefinitionClassRule.php | 4 +++- .../Rules/PhpDoc/SealedDefinitionClassRuleTest.php | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Rules/PhpDoc/SealedDefinitionClassRule.php b/src/Rules/PhpDoc/SealedDefinitionClassRule.php index a324f3afce4..a258389598d 100644 --- a/src/Rules/PhpDoc/SealedDefinitionClassRule.php +++ b/src/Rules/PhpDoc/SealedDefinitionClassRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\DependencyInjection\ValidatesStubFiles; use PHPStan\Node\InClassNode; @@ -87,7 +88,8 @@ public function processNode(Node $node, Scope $scope): array $sealedTypeReflection = $this->reflectionProvider->getClass($class); if ( - ($sealedTypeReflection->isEnum() || $sealedTypeReflection->isFinal()) + BleedingEdgeToggle::isBleedingEdge() + && ($sealedTypeReflection->isEnum() || $sealedTypeReflection->isFinal()) && !$sealedTypeReflection->is($classReflection->getName()) ) { $errorBuilder = RuleErrorBuilder::message(sprintf( diff --git a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php index ea2972097c1..d72b6bd22b1 100644 --- a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -87,4 +88,12 @@ public function testNonFinalSubtypes(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testFinalSubtypesAreNotCheckedWithoutBleedingEdge(): void + { + BleedingEdgeToggle::withBleedingEdge(false, function (): void { + $this->analyse([__DIR__ . '/data/sealed-non-final-subtypes.php'], []); + }); + } + } From d0d986185830531a82bba3ea43d85ab62f072699 Mon Sep 17 00:00:00 2001 From: klimick Date: Sat, 12 Sep 2026 20:07:47 +0300 Subject: [PATCH 4/5] Simplify sealed subtype test cases --- .../PhpDoc/SealedDefinitionClassRuleTest.php | 4 +- .../Rules/PhpDoc/data/sealed-subtypes.php | 45 ------------------- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php index d72b6bd22b1..983d9dd2693 100644 --- a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php @@ -68,11 +68,11 @@ public function testSubtypes(): void $this->analyse([__DIR__ . '/data/sealed-subtypes.php'], [ [ 'PHPDoc tag @phpstan-sealed contains final type SealedSubtypes\\__YEnumInvalid that is not subtype of SealedSubtypes\\__EnumError.', - 23, + 10, ], [ 'PHPDoc tag @phpstan-sealed contains final type SealedSubtypes\\__YClassInvalid that is not subtype of SealedSubtypes\\__ClassError.', - 65, + 20, ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php b/tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php index 840a5ef5ea8..70f35536e3c 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php +++ b/tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php @@ -2,60 +2,15 @@ namespace SealedSubtypes; -/** - * Subtyping is reflexive, so Reflexivity is a subtype of itself. - * This is a degenerate sealed declaration, but it is valid and must not report an error. - * - * @phpstan-sealed Reflexivity - */ -interface Reflexivity {} - /** @phpstan-sealed _XEnum */ interface _Enum {} enum _XEnum implements _Enum { case Value; } -/** @phpstan-sealed __XEnum | __YEnum */ -interface __Enum {} -enum __XEnum implements __Enum { case Value; } -enum __YEnum implements __Enum { case Value; } - /** @phpstan-sealed __XEnumValid | __YEnumInvalid */ interface __EnumError {} enum __XEnumValid implements __EnumError { case Value; } enum __YEnumInvalid { case Value; } -/** @phpstan-sealed _XInterface */ -interface _Interface {} -interface _XInterface extends _Interface {} - -/** @phpstan-sealed __XInterface | __YInterface */ -interface __Interface {} -interface __XInterface extends __Interface {} -interface __YInterface extends __Interface {} - -/** @phpstan-sealed __XInterfaceValid | __YInterfaceInvalid */ -interface __InterfaceError {} -interface __XInterfaceValid extends __InterfaceError {} -interface __YInterfaceInvalid {} - -/** @phpstan-sealed _XAbstractClass */ -abstract readonly class _AbstractClass {} -abstract readonly class _XAbstractClass extends _AbstractClass {} - -/** @phpstan-sealed __XAbstractClass | __YAbstractClass */ -abstract readonly class __AbstractClass {} -abstract readonly class __XAbstractClass extends __AbstractClass {} -abstract readonly class __YAbstractClass extends __AbstractClass {} - -/** @phpstan-sealed __XAbstractClassValid | __YAbstractClassInvalid */ -abstract readonly class __AbstractClassError {} -abstract readonly class __XAbstractClassValid extends __AbstractClassError {} -abstract readonly class __YAbstractClassInvalid {} - -/** @phpstan-sealed _XClass */ -abstract readonly class _Class {} -final readonly class _XClass extends _Class {} - /** @phpstan-sealed __XClass | __YClass */ abstract readonly class __Class {} final readonly class __XClass extends __Class {} From 919b37e30ab9821a09da0454c2696cb8564d7989 Mon Sep 17 00:00:00 2001 From: klimick Date: Sun, 13 Sep 2026 00:49:43 +0300 Subject: [PATCH 5/5] Use a dedicated feature toggle for sealed subtype checks --- conf/bleedingEdge.neon | 1 + conf/config.neon | 1 + conf/parametersSchema.neon | 1 + src/Rules/PhpDoc/SealedDefinitionClassRule.php | 5 +++-- .../Rules/PhpDoc/SealedDefinitionClassRuleTest.php | 11 ++++++----- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index a2f57a4871f..9bc27b265e8 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,6 +1,7 @@ parameters: featureToggles: bleedingEdge: true + checkSealedSubtypes: true checkNonStringableDynamicAccess: true checkParameterCastableToNumberFunctions: true skipCheckGenericClasses!: [] diff --git a/conf/config.neon b/conf/config.neon index 16e588434ac..9e5757598b7 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -26,6 +26,7 @@ parameters: throwTypeCovariance: false featureToggles: bleedingEdge: false + checkSealedSubtypes: false checkNonStringableDynamicAccess: false checkParameterCastableToNumberFunctions: false skipCheckGenericClasses: diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 5d968573089..4240bc01e24 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -30,6 +30,7 @@ parametersSchema: ]) featureToggles: structure([ bleedingEdge: bool(), + checkSealedSubtypes: bool(), checkNonStringableDynamicAccess: bool(), checkParameterCastableToNumberFunctions: bool(), skipCheckGenericClasses: listOf(string()), diff --git a/src/Rules/PhpDoc/SealedDefinitionClassRule.php b/src/Rules/PhpDoc/SealedDefinitionClassRule.php index a258389598d..6e46dc00033 100644 --- a/src/Rules/PhpDoc/SealedDefinitionClassRule.php +++ b/src/Rules/PhpDoc/SealedDefinitionClassRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; -use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\DependencyInjection\ValidatesStubFiles; use PHPStan\Node\InClassNode; @@ -35,6 +34,8 @@ public function __construct( private bool $checkClassCaseSensitivity, #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] private bool $discoveringSymbolsTip, + #[AutowiredParameter(ref: '%featureToggles.checkSealedSubtypes%')] + private bool $checkSealedSubtypes, ) { } @@ -88,7 +89,7 @@ public function processNode(Node $node, Scope $scope): array $sealedTypeReflection = $this->reflectionProvider->getClass($class); if ( - BleedingEdgeToggle::isBleedingEdge() + $this->checkSealedSubtypes && ($sealedTypeReflection->isEnum() || $sealedTypeReflection->isFinal()) && !$sealedTypeReflection->is($classReflection->getName()) ) { diff --git a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php index 983d9dd2693..4f2f8a2c1b7 100644 --- a/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php @@ -3,7 +3,6 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Classes\ForbiddenClassNameExtension; -use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -18,6 +17,8 @@ class SealedDefinitionClassRuleTest extends RuleTestCase { + private bool $checkSealedSubtypes = true; + protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); @@ -33,6 +34,7 @@ protected function getRule(): Rule ), true, true, + $this->checkSealedSubtypes, ); } @@ -89,11 +91,10 @@ public function testNonFinalSubtypes(): void } #[RequiresPhp('>= 8.1.0')] - public function testFinalSubtypesAreNotCheckedWithoutBleedingEdge(): void + public function testFinalSubtypesAreNotCheckedWhenDisabled(): void { - BleedingEdgeToggle::withBleedingEdge(false, function (): void { - $this->analyse([__DIR__ . '/data/sealed-non-final-subtypes.php'], []); - }); + $this->checkSealedSubtypes = false; + $this->analyse([__DIR__ . '/data/sealed-non-final-subtypes.php'], []); } }