Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
parameters:
featureToggles:
bleedingEdge: true
checkSealedSubtypes: true
checkNonStringableDynamicAccess: true
checkParameterCastableToNumberFunctions: true
skipCheckGenericClasses!: []
Expand Down
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ parameters:
throwTypeCovariance: false
featureToggles:
bleedingEdge: false
checkSealedSubtypes: false
checkNonStringableDynamicAccess: false
checkParameterCastableToNumberFunctions: false
skipCheckGenericClasses:
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ parametersSchema:
])
featureToggles: structure([
bleedingEdge: bool(),
checkSealedSubtypes: bool(),
checkNonStringableDynamicAccess: bool(),
checkParameterCastableToNumberFunctions: bool(),
skipCheckGenericClasses: listOf(string()),
Expand Down
20 changes: 20 additions & 0 deletions src/Rules/PhpDoc/SealedDefinitionClassRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public function __construct(
private bool $checkClassCaseSensitivity,
#[AutowiredParameter(ref: '%tips.discoveringSymbols%')]
private bool $discoveringSymbolsTip,
#[AutowiredParameter(ref: '%featureToggles.checkSealedSubtypes%')]
private bool $checkSealedSubtypes,
)
{
}
Expand Down Expand Up @@ -84,6 +86,24 @@ public function processNode(Node $node, Scope $scope): array
continue;
}

$sealedTypeReflection = $this->reflectionProvider->getClass($class);

if (
$this->checkSealedSubtypes
&& ($sealedTypeReflection->isEnum() || $sealedTypeReflection->isFinal())
&& !$sealedTypeReflection->is($classReflection->getName())
) {
$errorBuilder = RuleErrorBuilder::message(sprintf(
'PHPDoc tag @phpstan-sealed contains final type %s that is not subtype of %s.',
$class,
$classReflection->getName(),
))->identifier('sealed.notSubtype');

$errors[] = $errorBuilder->build();

continue;
}

$errors = array_merge(
$errors,
$this->classCheck->checkClassNames($scope, [
Expand Down
36 changes: 36 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/SealedDefinitionClassRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
class SealedDefinitionClassRuleTest extends RuleTestCase
{

private bool $checkSealedSubtypes = true;

protected function getRule(): Rule
{
$reflectionProvider = self::createReflectionProvider();
Expand All @@ -32,6 +34,7 @@ protected function getRule(): Rule
),
true,
true,
$this->checkSealedSubtypes,
);
}

Expand Down Expand Up @@ -61,4 +64,37 @@ public function testRule(): void
]);
}

#[RequiresPhp('>= 8.2.0')]
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.',
10,
],
[
'PHPDoc tag @phpstan-sealed contains final type SealedSubtypes\\__YClassInvalid that is not subtype of SealedSubtypes\\__ClassError.',
20,
],
]);
}

#[RequiresPhp('>= 8.1.0')]
public function testNonFinalSubtypes(): void
{
$this->analyse([__DIR__ . '/data/sealed-non-final-subtypes.php'], [
[
'PHPDoc tag @phpstan-sealed contains final type SealedNonFinalSubtypes\\InvalidZ that is not subtype of SealedNonFinalSubtypes\\InvalidSealed.',
8,
],
]);
}

#[RequiresPhp('>= 8.1.0')]
public function testFinalSubtypesAreNotCheckedWhenDisabled(): void
{
$this->checkSealedSubtypes = false;
$this->analyse([__DIR__ . '/data/sealed-non-final-subtypes.php'], []);
}

}
22 changes: 22 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/data/sealed-non-final-subtypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php // lint >= 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 {}
22 changes: 22 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/data/sealed-subtypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php // lint >= 8.2

namespace SealedSubtypes;

/** @phpstan-sealed _XEnum */
interface _Enum {}
enum _XEnum implements _Enum { case Value; }

/** @phpstan-sealed __XEnumValid | __YEnumInvalid */
interface __EnumError {}
enum __XEnumValid implements __EnumError { case Value; }
enum __YEnumInvalid { case Value; }

/** @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 {}
Loading