diff --git a/src/Reflection/Annotations/AnnotationPropertyReflection.php b/src/Reflection/Annotations/AnnotationPropertyReflection.php index 8f4f322d08..a79ffaaf3b 100644 --- a/src/Reflection/Annotations/AnnotationPropertyReflection.php +++ b/src/Reflection/Annotations/AnnotationPropertyReflection.php @@ -119,6 +119,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Reflection/Dummy/ChangedTypePropertyReflection.php b/src/Reflection/Dummy/ChangedTypePropertyReflection.php index 3bf6a6eb84..b43cf53453 100644 --- a/src/Reflection/Dummy/ChangedTypePropertyReflection.php +++ b/src/Reflection/Dummy/ChangedTypePropertyReflection.php @@ -116,6 +116,11 @@ public function isAbstract(): TrinaryLogic return $this->reflection->isAbstract(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->reflection->isFinalByKeyword(); + } + public function isFinal(): TrinaryLogic { return $this->reflection->isFinal(); diff --git a/src/Reflection/Dummy/DummyPropertyReflection.php b/src/Reflection/Dummy/DummyPropertyReflection.php index ca828a53fe..c90f546344 100644 --- a/src/Reflection/Dummy/DummyPropertyReflection.php +++ b/src/Reflection/Dummy/DummyPropertyReflection.php @@ -116,6 +116,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Reflection/ExtendedPropertyReflection.php b/src/Reflection/ExtendedPropertyReflection.php index 1027c193f1..ad8b5a26d8 100644 --- a/src/Reflection/ExtendedPropertyReflection.php +++ b/src/Reflection/ExtendedPropertyReflection.php @@ -38,6 +38,8 @@ public function getNativeType(): Type; public function isAbstract(): TrinaryLogic; + public function isFinalByKeyword(): TrinaryLogic; + public function isFinal(): TrinaryLogic; public function isVirtual(): TrinaryLogic; diff --git a/src/Reflection/Php/EnumPropertyReflection.php b/src/Reflection/Php/EnumPropertyReflection.php index 912b779e67..891491a1c5 100644 --- a/src/Reflection/Php/EnumPropertyReflection.php +++ b/src/Reflection/Php/EnumPropertyReflection.php @@ -112,6 +112,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index fc0dd70dbe..19144986c4 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -218,7 +218,7 @@ private function createProperty( $types[] = $value; } - return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, null, null, false, false, false, false, []); + return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, null, null, false, false, false, false, [], false); } } @@ -227,6 +227,7 @@ private function createProperty( $isDeprecated = $deprecation !== null; $isInternal = false; $isReadOnlyByPhpDoc = $classReflection->isImmutable(); + $isFinal = $classReflection->isFinal() || $propertyReflection->isFinal(); $isAllowedPrivateMutation = false; if ( @@ -308,6 +309,7 @@ private function createProperty( } $isInternal = $resolvedPhpDoc->isInternal(); $isReadOnlyByPhpDoc = $isReadOnlyByPhpDoc || $resolvedPhpDoc->isReadOnly(); + $isFinal = $isFinal || $resolvedPhpDoc->isFinal(); $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); } @@ -435,6 +437,7 @@ private function createProperty( $isReadOnlyByPhpDoc, $isAllowedPrivateMutation, $this->attributeReflectionFactory->fromNativeReflection($propertyReflection->getAttributes(), InitializerExprContext::fromClass($declaringClassReflection->getName(), $declaringClassReflection->getFileName())), + $isFinal, ); } diff --git a/src/Reflection/Php/PhpPropertyReflection.php b/src/Reflection/Php/PhpPropertyReflection.php index 8307f36d7b..8c4ea07439 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -44,6 +44,7 @@ public function __construct( private bool $isReadOnlyByPhpDoc, private bool $isAllowedPrivateMutation, private array $attributes, + private bool $isFinal, ) { } @@ -242,11 +243,16 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->reflection->isAbstract()); } - public function isFinal(): TrinaryLogic + public function isFinalByKeyword(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); } + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->isFinal); + } + public function isVirtual(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->reflection->isVirtual()); diff --git a/src/Reflection/Php/SimpleXMLElementProperty.php b/src/Reflection/Php/SimpleXMLElementProperty.php index 29975a5e3f..6fb9316fb5 100644 --- a/src/Reflection/Php/SimpleXMLElementProperty.php +++ b/src/Reflection/Php/SimpleXMLElementProperty.php @@ -127,6 +127,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Reflection/Php/UniversalObjectCrateProperty.php b/src/Reflection/Php/UniversalObjectCrateProperty.php index 564613e219..924b249e6f 100644 --- a/src/Reflection/Php/UniversalObjectCrateProperty.php +++ b/src/Reflection/Php/UniversalObjectCrateProperty.php @@ -117,6 +117,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Reflection/ResolvedPropertyReflection.php b/src/Reflection/ResolvedPropertyReflection.php index df7a33f84d..8b54c0785a 100644 --- a/src/Reflection/ResolvedPropertyReflection.php +++ b/src/Reflection/ResolvedPropertyReflection.php @@ -174,6 +174,11 @@ public function isAbstract(): TrinaryLogic return $this->reflection->isAbstract(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->reflection->isFinalByKeyword(); + } + public function isFinal(): TrinaryLogic { return $this->reflection->isFinal(); diff --git a/src/Reflection/Type/IntersectionTypePropertyReflection.php b/src/Reflection/Type/IntersectionTypePropertyReflection.php index 71de28e1e6..1e3e4d3179 100644 --- a/src/Reflection/Type/IntersectionTypePropertyReflection.php +++ b/src/Reflection/Type/IntersectionTypePropertyReflection.php @@ -148,6 +148,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract()); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinalByKeyword()); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::lazyMaxMin($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinal()); diff --git a/src/Reflection/Type/UnionTypePropertyReflection.php b/src/Reflection/Type/UnionTypePropertyReflection.php index 77e1ed0397..1862d3059f 100644 --- a/src/Reflection/Type/UnionTypePropertyReflection.php +++ b/src/Reflection/Type/UnionTypePropertyReflection.php @@ -148,6 +148,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isAbstract()); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinalByKeyword()); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::lazyExtremeIdentity($this->properties, static fn (ExtendedPropertyReflection $propertyReflection): TrinaryLogic => $propertyReflection->isFinal()); diff --git a/src/Reflection/WrappedExtendedPropertyReflection.php b/src/Reflection/WrappedExtendedPropertyReflection.php index 04eceb4848..ffb2a34363 100644 --- a/src/Reflection/WrappedExtendedPropertyReflection.php +++ b/src/Reflection/WrappedExtendedPropertyReflection.php @@ -109,6 +109,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Rules/Properties/FoundPropertyReflection.php b/src/Rules/Properties/FoundPropertyReflection.php index 1b14b785aa..b1a890b6be 100644 --- a/src/Rules/Properties/FoundPropertyReflection.php +++ b/src/Rules/Properties/FoundPropertyReflection.php @@ -143,6 +143,11 @@ public function isAbstract(): TrinaryLogic return $this->originalPropertyReflection->isAbstract(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->originalPropertyReflection->isFinalByKeyword(); + } + public function isFinal(): TrinaryLogic { return $this->originalPropertyReflection->isFinal(); diff --git a/src/Rules/Properties/OverridingPropertyRule.php b/src/Rules/Properties/OverridingPropertyRule.php index c1806565e2..8f4e23a861 100644 --- a/src/Rules/Properties/OverridingPropertyRule.php +++ b/src/Rules/Properties/OverridingPropertyRule.php @@ -135,7 +135,7 @@ public function processNode(Node $node, Scope $scope): array ))->identifier('property.visibility')->nonIgnorable()->build(); } - if ($prototype->isFinal()->yes()) { + if ($prototype->isFinalByKeyword()->yes()) { $errors[] = RuleErrorBuilder::message(sprintf( 'Property %s::$%s overrides final property %s::$%s.', $classReflection->getDisplayName(), @@ -145,6 +145,15 @@ public function processNode(Node $node, Scope $scope): array ))->identifier('property.parentPropertyFinal') ->nonIgnorable() ->build(); + } elseif ($prototype->isFinal()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s overrides @final property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->identifier('property.parentPropertyFinalByPhpDoc') + ->build(); } $typeErrors = []; diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php index 69eb2dbf4f..d6b0424be9 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassPropertyReflection.php @@ -73,6 +73,11 @@ public function isAbstract(): TrinaryLogic return $this->propertyReflection->isAbstract(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->propertyReflection->isFinalByKeyword(); + } + public function isFinal(): TrinaryLogic { return $this->propertyReflection->isFinal(); diff --git a/src/Type/ObjectShapePropertyReflection.php b/src/Type/ObjectShapePropertyReflection.php index 594c3278ca..1d022cb726 100644 --- a/src/Type/ObjectShapePropertyReflection.php +++ b/src/Type/ObjectShapePropertyReflection.php @@ -114,6 +114,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isFinal(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php index c5f5cd929c..dd9749e079 100644 --- a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php @@ -187,19 +187,27 @@ public function testFinal(): void $this->analyse([__DIR__ . '/data/overriding-final-property.php'], [ [ 'Property OverridingFinalProperty\Bar::$a overrides final property OverridingFinalProperty\Foo::$a.', - 21, + 27, ], [ 'Property OverridingFinalProperty\Bar::$b overrides final property OverridingFinalProperty\Foo::$b.', - 23, + 29, ], [ 'Property OverridingFinalProperty\Bar::$c overrides final property OverridingFinalProperty\Foo::$c.', - 25, + 31, ], [ 'Property OverridingFinalProperty\Bar::$d overrides final property OverridingFinalProperty\Foo::$d.', - 27, + 33, + ], + [ + 'Property OverridingFinalProperty\Bar::$e overrides @final property OverridingFinalProperty\Foo::$e.', + 35, + ], + [ + 'Property OverridingFinalProperty\Bar::$f overrides @final property OverridingFinalProperty\Foo::$f.', + 37, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/overriding-final-property.php b/tests/PHPStan/Rules/Properties/data/overriding-final-property.php index 02d7467e57..b41b531c98 100644 --- a/tests/PHPStan/Rules/Properties/data/overriding-final-property.php +++ b/tests/PHPStan/Rules/Properties/data/overriding-final-property.php @@ -13,6 +13,12 @@ class Foo protected private(set) int $d; + /** @final */ + public $e; + + /** @final */ + protected $f; + } class Bar extends Foo @@ -26,4 +32,8 @@ class Bar extends Foo public int $d; + public $e; + + protected $f; + } diff --git a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php index df34c15d50..cfe63c0dd7 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -167,55 +167,55 @@ public function testUnsetHookedProperty(): void ], [ 'Cannot unset property UnsetHookedProperty\NonFinalClass::$publicProperty because it might have hooks in a subclass.', - 13, + 14, ], [ 'Cannot unset property UnsetHookedProperty\ContainerClass::$finalClass because it might have hooks in a subclass.', - 83, + 86, ], [ 'Cannot unset property UnsetHookedProperty\ContainerClass::$nonFinalClass because it might have hooks in a subclass.', - 87, + 91, ], [ 'Cannot unset hooked UnsetHookedProperty\Foo::$iii property.', - 89, + 93, ], [ 'Cannot unset property UnsetHookedProperty\ContainerClass::$foo because it might have hooks in a subclass.', - 90, + 94, ], [ 'Cannot unset hooked UnsetHookedProperty\User::$name property.', - 92, + 96, ], [ 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', - 93, + 97, ], [ 'Cannot unset property UnsetHookedProperty\ContainerClass::$user because it might have hooks in a subclass.', - 94, + 98, ], [ 'Cannot unset hooked UnsetHookedProperty\User::$name property.', - 96, + 100, ], [ 'Cannot unset hooked UnsetHookedProperty\User::$name property.', - 97, + 101, ], [ 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', - 98, + 102, ], [ 'Cannot unset hooked UnsetHookedProperty\User::$fullName property.', - 99, + 103, ], [ 'Cannot unset property UnsetHookedProperty\ContainerClass::$arrayOfUsers because it might have hooks in a subclass.', - 100, + 104, ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php b/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php index d98eed672a..97ba5781cd 100644 --- a/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php +++ b/tests/PHPStan/Rules/Variables/data/unset-hooked-property.php @@ -9,6 +9,7 @@ function doUnset(Foo $foo, User $user, NonFinalClass $nonFinalClass, FinalClass unset($foo->ii); unset($foo->iii); + unset($nonFinalClass->publicAnnotatedFinalProperty); unset($nonFinalClass->publicFinalProperty); unset($nonFinalClass->publicProperty); @@ -49,6 +50,8 @@ class NonFinalClass { private string $privateProperty; public string $publicProperty; final public string $publicFinalProperty; + /** @final */ + public string $publicAnnotatedFinalProperty; function doFoo() { unset($this->privateProperty); @@ -82,6 +85,7 @@ function dooNestedUnset(ContainerClass $containerClass) { unset($containerClass->finalClass->publicProperty); unset($containerClass->finalClass); + unset($containerClass->nonFinalClass->publicAnnotatedFinalProperty); unset($containerClass->nonFinalClass->publicFinalProperty); unset($containerClass->nonFinalClass->publicProperty); unset($containerClass->nonFinalClass);