diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 21f17e12c..7ea948ce2 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -213,10 +213,14 @@ + + + + diff --git a/src/Reflection/ReflectionProperty.php b/src/Reflection/ReflectionProperty.php index 8fce08736..8761a32df 100644 --- a/src/Reflection/ReflectionProperty.php +++ b/src/Reflection/ReflectionProperty.php @@ -666,16 +666,32 @@ private function assertObject(mixed $object): object /** @return int-mask-of */ private function computeModifiers(PropertyNode $node): int { - $modifiers = $node->isReadonly() ? ReflectionPropertyAdapter::IS_READONLY : 0; + $modifiers = $node->isReadonly() ? CoreReflectionProperty::IS_READONLY : 0; $modifiers += $node->isStatic() ? CoreReflectionProperty::IS_STATIC : 0; $modifiers += $node->isPrivate() ? CoreReflectionProperty::IS_PRIVATE : 0; - $modifiers += $node->isPrivateSet() ? ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY : 0; + $modifiers += ! $node->isPrivate() && $node->isPrivateSet() ? ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY : 0; $modifiers += $node->isProtected() ? CoreReflectionProperty::IS_PROTECTED : 0; - $modifiers += $node->isProtectedSet() ? ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY : 0; + $modifiers += ! $node->isProtected() && $node->isProtectedSet() ? ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY : 0; $modifiers += $node->isPublic() ? CoreReflectionProperty::IS_PUBLIC : 0; $modifiers += $node->isFinal() ? ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY : 0; $modifiers += $node->isAbstract() ? ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY : 0; + if ( + ! ($modifiers & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY) + && ($modifiers & ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY) + ) { + $modifiers += ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY; + } + + if ( + ! ($modifiers & (ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY)) + && ! $node->isPublicSet() + && $node->isPublic() + && ($modifiers & CoreReflectionProperty::IS_READONLY) + ) { + $modifiers += ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY; + } + /** @phpstan-ignore return.type */ return $modifiers; } diff --git a/test/unit/Fixture/AsymetricVisibilityClass.php b/test/unit/Fixture/AsymmetricVisibilityClass.php similarity index 96% rename from test/unit/Fixture/AsymetricVisibilityClass.php rename to test/unit/Fixture/AsymmetricVisibilityClass.php index 4e7bb1a0b..8241b99c5 100644 --- a/test/unit/Fixture/AsymetricVisibilityClass.php +++ b/test/unit/Fixture/AsymmetricVisibilityClass.php @@ -2,7 +2,7 @@ namespace Roave\BetterReflectionTest\Fixture; -class AsymetricVisibilityClass +class AsymmetricVisibilityClass { public public(set) string $publicPublicSet = 'string'; public protected(set) string $publicProtectedSet = 'string'; @@ -22,4 +22,3 @@ public function __construct( { } } - diff --git a/test/unit/Fixture/AsymetricVisibilityClassExport.txt b/test/unit/Fixture/AsymmetricVisibilityClassExport.txt similarity index 87% rename from test/unit/Fixture/AsymetricVisibilityClassExport.txt rename to test/unit/Fixture/AsymmetricVisibilityClassExport.txt index 5c5f3f270..369c8005b 100644 --- a/test/unit/Fixture/AsymetricVisibilityClassExport.txt +++ b/test/unit/Fixture/AsymmetricVisibilityClassExport.txt @@ -1,5 +1,5 @@ -Class [ class Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass ] { - @@ %s/Fixture/AsymetricVisibilityClass.php 5-24 +Class [ class Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityClass ] { + @@ %s/Fixture/AsymmetricVisibilityClass.php 5-24 - Constants [0] { } @@ -27,7 +27,7 @@ Class [ class Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass - Methods [1] { Method [ public method __construct ] { - @@ %s/Fixture/AsymetricVisibilityClass.php 14 - 23 + @@ %s/Fixture/AsymmetricVisibilityClass.php 14 - 23 - Parameters [6] { Parameter #0 [ string $promotedPublicPublicSet ] diff --git a/test/unit/Fixture/AsymmetricVisibilityImplicitFinal.php b/test/unit/Fixture/AsymmetricVisibilityImplicitFinal.php new file mode 100644 index 000000000..0cbaf596e --- /dev/null +++ b/test/unit/Fixture/AsymmetricVisibilityImplicitFinal.php @@ -0,0 +1,10 @@ +, 1: int}> */ - public static function getPropertiesWithAsymetricVisibilityFilterDataProvider(): array + public static function getPropertiesWithAsymmetricVisibilityFilterDataProvider(): array { return [ [CoreReflectionProperty::IS_PUBLIC, 6], [CoreReflectionProperty::IS_PROTECTED, 4], [CoreReflectionProperty::IS_PRIVATE, 2], - [ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY, 4], - [ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY, 6], + [ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY, 2], + [ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY, 4], [ CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY, - 10, + 8, ], ]; } /** @param int-mask-of $filter */ - #[DataProvider('getPropertiesWithAsymetricVisibilityFilterDataProvider')] - public function testGetPropertiesWithAsymetricVisibilityFilter(int $filter, int $count): void + #[DataProvider('getPropertiesWithAsymmetricVisibilityFilterDataProvider')] + public function testGetPropertiesWithAsymmetricVisibilityFilter(int $filter, int $count): void { - $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymetricVisibilityClass.php', $this->astLocator)); - $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass'); + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymmetricVisibilityClass.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityClass'); self::assertCount($count, $classInfo->getProperties($filter)); self::assertCount($count, $classInfo->getImmediateProperties($filter)); @@ -2308,13 +2308,13 @@ public function testToString(): void ); } - public function testToStringWithAsymetricVisibility(): void + public function testToStringWithAsymmetricVisibility(): void { - $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymetricVisibilityClass.php', $this->astLocator)); - $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass'); + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymmetricVisibilityClass.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityClass'); self::assertStringMatchesFormat( - file_get_contents(__DIR__ . '/../Fixture/AsymetricVisibilityClassExport.txt'), + file_get_contents(__DIR__ . '/../Fixture/AsymmetricVisibilityClassExport.txt'), $classInfo->__toString(), ); } diff --git a/test/unit/Reflection/ReflectionPropertyTest.php b/test/unit/Reflection/ReflectionPropertyTest.php index 1659f54ea..4ccff5977 100644 --- a/test/unit/Reflection/ReflectionPropertyTest.php +++ b/test/unit/Reflection/ReflectionPropertyTest.php @@ -148,6 +148,24 @@ public function testVisibilityMethods(): void self::assertFalse($onlyPublicProp->isReadOnly()); } + public function isImplicitPublic(): void + { + $php = <<<'PHP' + astLocator)))->reflectClass('Foo'); + $propertyReflection = $classReflection->getProperty('boo'); + + self::assertTrue($propertyReflection->isPublic()); + self::assertSame(CoreReflectionProperty::IS_PUBLIC, $propertyReflection->getModifiers()); + } + public function testIsStatic(): void { $classInfo = $this->reflector->reflectClass(ExampleClass::class); @@ -254,7 +272,7 @@ public static function modifierProvider(): array ['protectedProperty', CoreReflectionProperty::IS_PROTECTED], ['privateProperty', CoreReflectionProperty::IS_PRIVATE], ['publicStaticProperty', CoreReflectionProperty::IS_PUBLIC | CoreReflectionProperty::IS_STATIC], - ['readOnlyProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_READONLY], + ['readOnlyProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_READONLY | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], ['finalPublicProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY], ]; } @@ -897,30 +915,30 @@ public function testWithImplementingClass(): void } /** @return list}> */ - public static function asymetricVisibilityModifierProvider(): array + public static function asymmetricVisibilityModifierProvider(): array { return [ ['publicPublicSet', CoreReflectionProperty::IS_PUBLIC], ['publicProtectedSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], - ['publicPrivateSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], - ['protectedProtectedSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], - ['protectedPrivateSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], - ['privatePrivateSet', CoreReflectionProperty::IS_PRIVATE | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], + ['publicPrivateSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY], + ['protectedProtectedSet', CoreReflectionProperty::IS_PROTECTED], + ['protectedPrivateSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY], + ['privatePrivateSet', CoreReflectionProperty::IS_PRIVATE], ['promotedPublicPublicSet', CoreReflectionProperty::IS_PUBLIC], ['promotedPublicProtectedSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], - ['promotedPublicPrivateSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], - ['promotedProtectedProtectedSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], - ['promotedProtectedPrivateSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], - ['promotedPrivatePrivateSet', CoreReflectionProperty::IS_PRIVATE | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], + ['promotedPublicPrivateSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY], + ['promotedProtectedProtectedSet', CoreReflectionProperty::IS_PROTECTED], + ['promotedProtectedPrivateSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY], + ['promotedPrivatePrivateSet', CoreReflectionProperty::IS_PRIVATE], ]; } /** @param non-empty-string $propertyName */ - #[DataProvider('asymetricVisibilityModifierProvider')] - public function testGetAsymetricVisibilityMethods(string $propertyName, int $expectedModifier): void + #[DataProvider('asymmetricVisibilityModifierProvider')] + public function testGetAsymmetricVisibilityMethods(string $propertyName, int $expectedModifier): void { - $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymetricVisibilityClass.php', $this->astLocator)); - $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass'); + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymmetricVisibilityClass.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityClass'); $property = $classInfo->getProperty($propertyName); self::assertSame($expectedModifier, $property->getModifiers()); @@ -928,8 +946,8 @@ public function testGetAsymetricVisibilityMethods(string $propertyName, int $exp public function testIsProtectedSet(): void { - $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymetricVisibilityClass.php', $this->astLocator)); - $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass'); + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymmetricVisibilityClass.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityClass'); $publicPublicSetProperty = $classInfo->getProperty('publicPublicSet'); $publicProtectedSetProperty = $classInfo->getProperty('publicProtectedSet'); @@ -940,8 +958,8 @@ public function testIsProtectedSet(): void public function testIsPrivateSet(): void { - $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymetricVisibilityClass.php', $this->astLocator)); - $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass'); + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymmetricVisibilityClass.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityClass'); $protectedProtectedSet = $classInfo->getProperty('protectedProtectedSet'); $protectedPrivateSet = $classInfo->getProperty('protectedPrivateSet'); @@ -950,6 +968,50 @@ public function testIsPrivateSet(): void self::assertTrue($protectedPrivateSet->isPrivateSet()); } + /** @return list */ + public static function asymmetricVisibilityImplicitFinalProvider(): array + { + return [ + ['publicPrivateSetIsFinal', true], + ['protectedPrivateSetIsFinal', true], + ['privatePrivateSetIsNotFinal', false], + ]; + } + + /** @param non-empty-string $propertyName */ + #[DataProvider('asymmetricVisibilityImplicitFinalProvider')] + public function testAsymmetricVisibilityImplicitFinal(string $propertyName, bool $isFinal): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymmetricVisibilityImplicitFinal.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityImplicitFinal'); + $property = $classInfo->getProperty($propertyName); + + self::assertSame($isFinal, $property->isFinal()); + } + + /** @return list */ + public static function asymmetricVisibilityImplicitProtectedSetProvider(): array + { + return [ + ['publicPublicSet', false], + ['publicProtectedSet', true], + ['publicPrivateSet', false], + ['protected', false], + ['publicImplicitProtectedSet', true], + ]; + } + + /** @param non-empty-string $propertyName */ + #[DataProvider('asymmetricVisibilityImplicitProtectedSetProvider')] + public function testAsymmetricVisibilityImplicitProtectedSet(string $propertyName, bool $isProtectedSet): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymmetricVisibilityImplicitProtectedSet.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityImplicitProtectedSet'); + $property = $classInfo->getProperty($propertyName); + + self::assertSame($isProtectedSet, $property->isProtectedSet()); + } + public function testIsAbstract(): void { $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator));