From 3bf83318fb9ea278122c234c5cfdc44ffbc7c3e1 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 28 Mar 2025 12:34:07 +0100 Subject: [PATCH 01/27] WIP Instance vs Static properties --- src/Reflection/ClassReflection.php | 163 +++++++++++++++++- .../Php/PhpClassReflectionExtension.php | 42 ++++- ...endsPropertiesClassReflectionExtension.php | 96 ++++++++++- ...PropertiesClassReflectionExtensionTest.php | 4 +- .../Annotations/DeprecatedAnnotationsTest.php | 12 +- .../Annotations/InternalAnnotationsTest.php | 15 +- 6 files changed, 318 insertions(+), 14 deletions(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 3c9ae93e13..eef4e9860d 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -83,6 +83,12 @@ final class ClassReflection /** @var ExtendedPropertyReflection[] */ private array $properties = []; + /** @var ExtendedPropertyReflection[] */ + private array $instanceProperties = []; + + /** @var ExtendedPropertyReflection[] */ + private array $staticProperties = []; + /** @var RealClassClassConstantReflection[] */ private array $constants = []; @@ -149,6 +155,12 @@ final class ClassReflection /** @var array */ private array $hasPropertyCache = []; + /** @var array */ + private array $hasInstancePropertyCache = []; + + /** @var array */ + private array $hasStaticPropertyCache = []; + /** * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions @@ -449,6 +461,9 @@ public function allowsDynamicProperties(): bool return $attributes !== []; } + /** + * @deprecated Use hasInstanceProperty or hasStaticProperty instead + */ public function hasProperty(string $propertyName): bool { if (array_key_exists($propertyName, $this->hasPropertyCache)) { @@ -468,6 +483,11 @@ public function hasProperty(string $propertyName): bool } } + // For BC purpose + if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { + return $this->hasPropertyCache[$propertyName] = true; + } + if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { return $this->hasPropertyCache[$propertyName] = true; } @@ -475,6 +495,49 @@ public function hasProperty(string $propertyName): bool return $this->hasPropertyCache[$propertyName] = false; } + public function hasInstanceProperty(string $propertyName): bool + { + if (array_key_exists($propertyName, $this->hasInstancePropertyCache)) { + return $this->hasInstancePropertyCache[$propertyName]; + } + + if ($this->isEnum()) { + return $this->hasInstancePropertyCache[$propertyName] = $this->hasNativeProperty($propertyName); + } + + foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { + if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { + break; + } + if ($extension->hasProperty($this, $propertyName)) { + return $this->hasInstancePropertyCache[$propertyName] = true; + } + } + + if ($this->requireExtendsPropertiesClassReflectionExtension->hasInstanceProperty($this, $propertyName)) { + return $this->hasPropertyCache[$propertyName] = true; + } + + return $this->hasPropertyCache[$propertyName] = false; + } + + public function hasStaticProperty(string $propertyName): bool + { + if (array_key_exists($propertyName, $this->hasStaticPropertyCache)) { + return $this->hasStaticPropertyCache[$propertyName]; + } + + if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { + return $this->hasStaticPropertyCache[$propertyName] = true; + } + + if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { + return $this->hasStaticPropertyCache[$propertyName] = true; + } + + return $this->hasStaticPropertyCache[$propertyName] = false; + } + public function hasMethod(string $methodName): bool { if (array_key_exists($methodName, $this->hasMethodCache)) { @@ -619,6 +682,20 @@ public function evictPrivateSymbols(): void unset($this->properties[$name]); } + foreach ($this->instanceProperties as $name => $property) { + if (!$property->isPrivate()) { + continue; + } + + unset($this->instanceProperties[$name]); + } + foreach ($this->staticProperties as $name => $property) { + if (!$property->isPrivate()) { + continue; + } + + unset($this->staticProperties[$name]); + } foreach ($this->methods as $name => $method) { if (!$method->isPrivate()) { continue; @@ -629,6 +706,7 @@ public function evictPrivateSymbols(): void $this->getPhpExtension()->evictPrivateSymbols($this->getCacheKey()); } + /** @deprecated Use getInstanceProperty or getStaticProperty */ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { if ($this->isEnum()) { @@ -658,6 +736,15 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco } } + // For BC purpose + if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { + $property = $this->wrapExtendedProperty($this->getPhpExtension()->getStaticProperty($this, $propertyName)); + if ($scope->canReadProperty($property)) { + return $this->properties[$key] = $property; + } + $this->properties[$key] = $property; + } + if (!isset($this->properties[$key])) { if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { $property = $this->requireExtendsPropertiesClassReflectionExtension->getProperty($this, $propertyName); @@ -672,9 +759,83 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco return $this->properties[$key]; } + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + if ($this->isEnum()) { + return $this->getNativeProperty($propertyName); + } + + $key = $propertyName; + if ($scope->isInClass()) { + $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey()); + } + + if (!isset($this->instanceProperties[$key])) { + foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { + if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { + break; + } + + if (!$extension->hasProperty($this, $propertyName)) { + continue; + } + + $property = $this->wrapExtendedProperty($extension->getProperty($this, $propertyName)); + if ($scope->canReadProperty($property)) { + return $this->instanceProperties[$key] = $property; + } + $this->instanceProperties[$key] = $property; + } + } + + if (!isset($this->instanceProperties[$key])) { + if ($this->requireExtendsPropertiesClassReflectionExtension->hasInstanceProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getInstanceProperty($this, $propertyName); + $this->instanceProperties[$key] = $property; + } + } + + if (!isset($this->instanceProperties[$key])) { + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); + } + + return $this->instanceProperties[$key]; + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + $key = $propertyName; + if ($scope->isInClass()) { + $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey()); + } + + if (!isset($this->staticProperties[$key])) { + if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { + $property = $this->wrapExtendedProperty($this->getPhpExtension()->getStaticProperty($this, $propertyName)); + if ($scope->canReadProperty($property)) { + return $this->staticProperties[$key] = $property; + } + $this->staticProperties[$key] = $property; + } + } + + if (!isset($this->staticProperties[$key])) { + if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getStaticProperty($this, $propertyName); + $this->staticProperties[$key] = $property; + } + } + + if (!isset($this->staticProperties[$key])) { + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); + } + + return $this->staticProperties[$key]; + } + public function hasNativeProperty(string $propertyName): bool { - return $this->getPhpExtension()->hasProperty($this, $propertyName); + return $this->getPhpExtension()->hasNativeProperty($this, $propertyName); } public function getNativeProperty(string $propertyName): PhpPropertyReflection diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 2970ef2f80..6fb0f55388 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -71,6 +71,9 @@ final class PhpClassReflectionExtension /** @var PhpPropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; + /** @var ExtendedPropertyReflection[][] */ + private array $staticPropertiesIncludingAnnotations = []; + /** @var PhpPropertyReflection[][] */ private array $nativeProperties = []; @@ -118,6 +121,17 @@ public function evictPrivateSymbols(string $classCacheKey): void unset($this->propertiesIncludingAnnotations[$key][$name]); } } + foreach ($this->staticPropertiesIncludingAnnotations as $key => $properties) { + if ($key !== $classCacheKey) { + continue; + } + foreach ($properties as $name => $property) { + if (!$property->isPrivate()) { + continue; + } + unset($this->staticPropertiesIncludingAnnotations[$key][$name]); + } + } foreach ($this->nativeProperties as $key => $properties) { if ($key !== $classCacheKey) { continue; @@ -155,7 +169,10 @@ public function evictPrivateSymbols(string $classCacheKey): void public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - return $classReflection->getNativeReflection()->hasProperty($propertyName); + $nativeReflection = $classReflection->getNativeReflection(); + + return $nativeReflection->hasProperty($propertyName) + && !$nativeReflection->getProperty($propertyName)->isStatic(); } public function getProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection @@ -167,6 +184,28 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa return $this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName]; } + public function hasStaticProperty(ClassReflection $classReflection, string $propertyName): bool + { + $nativeReflection = $classReflection->getNativeReflection(); + + return $nativeReflection->hasProperty($propertyName) + && $nativeReflection->getProperty($propertyName)->isStatic(); + } + + public function getStaticProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + if (!isset($this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName])) { + $this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName] = $this->createProperty($classReflection, $propertyName, true); + } + + return $this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName]; + } + + public function hasNativeProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $classReflection->getNativeReflection()->hasProperty($propertyName); + } + public function getNativeProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection { if (!isset($this->nativeProperties[$classReflection->getCacheKey()][$propertyName])) { @@ -177,6 +216,7 @@ public function getNativeProperty(ClassReflection $classReflection, string $prop return $this->nativeProperties[$classReflection->getCacheKey()][$propertyName]; } + // TODO: Find the difference between createInstanceProperty and createStaticProperty private function createProperty( ClassReflection $classReflection, string $propertyName, diff --git a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php index 550a7bee59..a0615b82f2 100644 --- a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php +++ b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php @@ -5,20 +5,33 @@ use PHPStan\Analyser\OutOfClassScope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedPropertyReflection; -use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Type; -final class RequireExtendsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension +final class RequireExtendsPropertiesClassReflectionExtension { + /** @deprecated Use hasInstanceProperty or hasStaticProperty */ public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - return $this->findProperty($classReflection, $propertyName) !== null; + return $this->findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + ) !== null; } + /** @deprecated Use getInstanceProperty or getStaticProperty */ public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection { - $property = $this->findProperty($classReflection, $propertyName); + $property = $this->findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + ); if ($property === null) { throw new ShouldNotHappenException(); } @@ -26,7 +39,74 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa return $property; } - private function findProperty(ClassReflection $classReflection, string $propertyName): ?ExtendedPropertyReflection + public function hasInstanceProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findProperty( + $classReflection, + $propertyName, + // TODO: Use hasInstanceProperty + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), + // TODO: Use getInstanceProperty + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + ) !== null; + } + + public function getInstanceProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty( + $classReflection, + $propertyName, + // TODO: Use hasInstanceProperty + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), + // TODO: Use getInstanceProperty + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + ); + if ($property === null) { + throw new ShouldNotHappenException(); + } + + return $property; + } + + public function hasStaticProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findProperty( + $classReflection, + $propertyName, + // TODO: Use hasStaticProperty + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), + // TODO: Use getStaticProperty + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + ) !== null; + } + + public function getStaticProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty( + $classReflection, + $propertyName, + // TODO: Use hasStaticProperty + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), + // TODO: Use getStaticProperty + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + ); + if ($property === null) { + throw new ShouldNotHappenException(); + } + + return $property; + } + + /** + * @param callable(Type, string): TrinaryLogic $propertyHasser + * @param callable(Type, string): ExtendedPropertyReflection $propertyGetter + */ + private function findProperty( + ClassReflection $classReflection, + string $propertyName, + callable $propertyHasser, + callable $propertyGetter, + ): ?ExtendedPropertyReflection { if (!$classReflection->isInterface()) { return null; @@ -36,16 +116,16 @@ private function findProperty(ClassReflection $classReflection, string $property foreach ($requireExtendsTags as $requireExtendsTag) { $type = $requireExtendsTag->getType(); - if (!$type->hasProperty($propertyName)->yes()) { + if (!$propertyHasser($type, $propertyName)->yes()) { continue; } - return $type->getProperty($propertyName, new OutOfClassScope()); + return $propertyGetter($type, $propertyName); } $interfaces = $classReflection->getInterfaces(); foreach ($interfaces as $interface) { - $property = $this->findProperty($interface, $propertyName); + $property = $this->findProperty($interface, $propertyName, $propertyHasser, $propertyGetter); if ($property !== null) { return $property; } diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php index 9e75d926b3..f654fdbb51 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php @@ -288,11 +288,11 @@ public function testProperties(string $className, array $properties): void $scope->method('canWriteProperty')->willReturn(true); foreach ($properties as $propertyName => $expectedPropertyData) { $this->assertTrue( - $class->hasProperty($propertyName), + $class->hasInstanceProperty($propertyName), sprintf('Class %s does not define property %s.', $className, $propertyName), ); - $property = $class->getProperty($propertyName, $scope); + $property = $class->getInstanceProperty($propertyName, $scope); $this->assertSame( $expectedPropertyData['class'], $property->getDeclaringClass()->getName(), diff --git a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php index b48fa67162..5db5d1304c 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -40,6 +40,8 @@ public static function dataDeprecatedAnnotations(): array ], 'property' => [ 'foo' => null, + ], + 'staticProperty' => [ 'staticFoo' => null, ], ], @@ -58,6 +60,8 @@ public static function dataDeprecatedAnnotations(): array ], 'property' => [ 'deprecatedFoo' => null, + ], + 'staticProperty' => [ 'deprecatedStaticFoo' => null, ], ], @@ -114,7 +118,13 @@ public function testDeprecatedAnnotations(bool $deprecated, string $className, ? } foreach ($deprecatedAnnotations['property'] ?? [] as $propertyName => $deprecatedMessage) { - $propertyAnnotation = $class->getProperty($propertyName, $scope); + $propertyAnnotation = $class->getInstanceProperty($propertyName, $scope); + $this->assertSame($deprecated, $propertyAnnotation->isDeprecated()->yes()); + $this->assertSame($deprecatedMessage, $propertyAnnotation->getDeprecatedDescription()); + } + + foreach ($deprecatedAnnotations['staticProperty'] ?? [] as $propertyName => $deprecatedMessage) { + $propertyAnnotation = $class->getStaticProperty($propertyName, $scope); $this->assertSame($deprecated, $propertyAnnotation->isDeprecated()->yes()); $this->assertSame($deprecatedMessage, $propertyAnnotation->getDeprecatedDescription()); } diff --git a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php index 79017d46cb..fa6110785c 100644 --- a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php @@ -32,6 +32,8 @@ public static function dataInternalAnnotations(): array ], 'property' => [ 'foo', + ], + 'staticProperty' => [ 'staticFoo', ], ], @@ -49,6 +51,8 @@ public static function dataInternalAnnotations(): array ], 'property' => [ 'internalFoo', + ], + 'staticProperty' => [ 'internalStaticFoo', ], ], @@ -89,6 +93,8 @@ public static function dataInternalAnnotations(): array ], 'property' => [ 'foo', + ], + 'staticProperty' => [ 'staticFoo', ], ], @@ -103,6 +109,8 @@ public static function dataInternalAnnotations(): array ], 'property' => [ 'internalFoo', + ], + 'staticProperty' => [ 'internalStaticFoo', ], ], @@ -133,7 +141,12 @@ public function testInternalAnnotations(bool $internal, string $className, array } foreach ($internalAnnotations['property'] ?? [] as $propertyName) { - $propertyAnnotation = $class->getProperty($propertyName, $scope); + $propertyAnnotation = $class->getInstanceProperty($propertyName, $scope); + $this->assertSame($internal, $propertyAnnotation->isInternal()->yes()); + } + + foreach ($internalAnnotations['staticProperty'] ?? [] as $propertyName) { + $propertyAnnotation = $class->getStaticProperty($propertyName, $scope); $this->assertSame($internal, $propertyAnnotation->isInternal()->yes()); } From 3a4f9e007d32502e42454f81710dc3086d9bbb70 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 28 Mar 2025 13:37:10 +0100 Subject: [PATCH 02/27] Solve deprecations --- src/Rules/Properties/AccessPropertiesCheck.php | 9 ++++++--- src/Rules/Properties/AccessStaticPropertiesRule.php | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index 4f135f462c..f45fa4c598 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -101,6 +101,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $scope, NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), + // TODO use hasInstanceProperty static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(), ); $type = $typeResult->getType(); @@ -127,6 +128,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ]; } + // TODO use hasInstanceProperty $has = $type->hasProperty($name); if ($has->maybe()) { if ($scope->isUndefinedExpressionAllowed($node)) { @@ -167,12 +169,12 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); $parentClassReflection = $propertyClassReflection->getParentClass(); while ($parentClassReflection !== null) { - if ($parentClassReflection->hasProperty($name)) { + if ($parentClassReflection->hasInstanceProperty($name)) { if ($write) { - if ($scope->canWriteProperty($parentClassReflection->getProperty($name, $scope))) { + if ($scope->canWriteProperty($parentClassReflection->getInstanceProperty($name, $scope))) { return []; } - } elseif ($scope->canReadProperty($parentClassReflection->getProperty($name, $scope))) { + } elseif ($scope->canReadProperty($parentClassReflection->getInstanceProperty($name, $scope))) { return []; } @@ -216,6 +218,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ]; } + // TODO use getInstanceProperty $propertyReflection = $type->getProperty($name, $scope); if ($propertyReflection->isStatic()) { return [ diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 2e52ae7202..b845f7970d 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -155,6 +155,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $scope, NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class), sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), + // TODO Use hasStaticProperty static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(), ); $classType = $classTypeResult->getType(); @@ -187,6 +188,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]); } + // TODO Use hasStaticProperty $has = $classType->hasProperty($name); if (!$has->no() && $scope->isUndefinedExpressionAllowed($node)) { return []; @@ -203,8 +205,8 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $parentClassReflection = $propertyClassReflection->getParentClass(); while ($parentClassReflection !== null) { - if ($parentClassReflection->hasProperty($name)) { - if ($scope->canReadProperty($parentClassReflection->getProperty($name, $scope))) { + if ($parentClassReflection->hasStaticProperty($name)) { + if ($scope->canReadProperty($parentClassReflection->getStaticProperty($name, $scope))) { return []; } return [ @@ -229,6 +231,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]); } + // TODO Use getStaticProperty and update the if $property = $classType->getProperty($name, $scope); if (!$property->isStatic()) { $hasPropertyTypes = TypeUtils::getHasPropertyTypes($classType); From e9fe52f16b37176010eca6b03e5f0a5203b09cc9 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 28 Mar 2025 14:31:15 +0100 Subject: [PATCH 03/27] Introduce new methods --- src/Analyser/MutatingScope.php | 31 +++- src/Analyser/Scope.php | 5 + src/Type/ClosureType.php | 30 +++ src/Type/Enum/EnumCaseObjectType.php | 25 ++- src/Type/Generic/GenericObjectType.php | 24 +++ src/Type/IntersectionType.php | 66 +++++++ src/Type/MixedType.php | 42 +++++ src/Type/NeverType.php | 30 +++ src/Type/NonexistentParentClassType.php | 30 +++ src/Type/ObjectShapeType.php | 38 +++- src/Type/ObjectType.php | 174 ++++++++++++++++++ src/Type/StaticType.php | 64 +++++++ src/Type/StrictMixedType.php | 30 +++ src/Type/Traits/LateResolvableTypeTrait.php | 30 +++ src/Type/Traits/MaybeObjectTypeTrait.php | 42 +++++ src/Type/Traits/NonObjectTypeTrait.php | 30 +++ src/Type/Traits/ObjectTypeTrait.php | 42 +++++ src/Type/Type.php | 15 ++ src/Type/UnionType.php | 66 +++++++ .../data/class-implements-out-of-phpstan.php | 30 +++ 20 files changed, 838 insertions(+), 6 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index ef4a78bd29..fae28b1e26 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -6204,7 +6204,10 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall); } - /** @api */ + /** + * @api + * @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead + */ public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection { if ($typeWithProperty instanceof UnionType) { @@ -6217,6 +6220,32 @@ public function getPropertyReflection(Type $typeWithProperty, string $propertyNa return $typeWithProperty->getProperty($propertyName, $this); } + /** @api */ + public function getInstancePropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection + { + if ($typeWithProperty instanceof UnionType) { + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasInstanceProperty($propertyName)->yes()); + } + if (!$typeWithProperty->hasInstanceProperty($propertyName)->yes()) { + return null; + } + + return $typeWithProperty->getInstanceProperty($propertyName, $this); + } + + /** @api */ + public function getStaticPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection + { + if ($typeWithProperty instanceof UnionType) { + $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasStaticProperty($propertyName)->yes()); + } + if (!$typeWithProperty->hasStaticProperty($propertyName)->yes()) { + return null; + } + + return $typeWithProperty->getStaticProperty($propertyName, $this); + } + /** * @param PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index a33b06a377..d7397399f6 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -75,8 +75,13 @@ public function getMaybeDefinedVariables(): array; public function hasConstant(Name $name): bool; + /** @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead */ public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; + public function getInstancePropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; + + public function getStaticPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection; public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection; diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 292557f12a..74c804e887 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -316,6 +316,36 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return $this->objectType->getUnresolvedPropertyPrototype($propertyName, $scope); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->objectType->hasInstanceProperty($propertyName); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->objectType->getInstanceProperty($propertyName, $scope); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->objectType->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->objectType->hasStaticProperty($propertyName); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->objectType->getStaticProperty($propertyName, $scope); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->objectType->getUnresolvedStaticPropertyPrototype($propertyName, $scope); + } + public function canCallMethods(): TrinaryLogic { return $this->objectType->canCallMethods(); diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index 302b93a8c8..ab1091846a 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -8,6 +8,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Php\EnumPropertyReflection; use PHPStan\Reflection\Php\EnumUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; @@ -127,10 +128,15 @@ public function tryRemove(Type $typeToRemove): ?Type } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { $classReflection = $this->getClassReflection(); if ($classReflection === null) { - return parent::getUnresolvedPropertyPrototype($propertyName, $scope); + return parent::getUnresolvedInstancePropertyPrototype($propertyName, $scope); } if ($propertyName === 'name') { @@ -153,7 +159,22 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember } } - return parent::getUnresolvedPropertyPrototype($propertyName, $scope); + return parent::getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); } public function getBackingValueType(): ?Type diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index a2bdadd7ae..54bef86b29 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -232,6 +232,30 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return $prototype->doNotResolveTemplateTypeMapToBounds(); } + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $prototype = parent::getUnresolvedInstancePropertyPrototype($propertyName, $scope); + + return $prototype->doNotResolveTemplateTypeMapToBounds(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $prototype = parent::getUnresolvedStaticPropertyPrototype($propertyName, $scope); + + return $prototype->doNotResolveTemplateTypeMapToBounds(); + } + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 142f1f6899..d643f66c18 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -538,6 +538,72 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName)); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasProperty($propertyName)->yes()) { + continue; + } + + $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new ShouldNotHappenException(); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName)); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasStaticProperty($propertyName)->yes()) { + continue; + } + + $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new ShouldNotHappenException(); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); + } + public function canCallMethods(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods()); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 11d60685b5..31df320708 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -422,6 +422,48 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection(); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection(); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + public function canCallMethods(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 659368d6da..5fc9a05280 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -138,6 +138,36 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember throw new ShouldNotHappenException(); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + public function canCallMethods(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index 0b91e093e6..1d5153f731 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -77,6 +77,36 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember throw new ShouldNotHappenException(); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + public function canCallMethods(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index 8b32331b36..6677f4ffa9 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -90,6 +90,21 @@ public function getObjectClassReflections(): array } public function hasProperty(string $propertyName): TrinaryLogic + { + return $this->hasInstanceProperty($propertyName); + } + + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getInstanceProperty($propertyName, $scope); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function hasInstanceProperty(string $propertyName): TrinaryLogic { if (!array_key_exists($propertyName, $this->properties)) { return TrinaryLogic::createNo(); @@ -102,12 +117,12 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createYes(); } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { - return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); } - public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { if (!array_key_exists($propertyName, $this->properties)) { throw new ShouldNotHappenException(); @@ -122,6 +137,23 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + // TODO Change the implementation + return $this->hasInstanceProperty($propertyName); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + // TODO Change the implementation + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 3512e998df..558f1fa9f5 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -102,6 +102,12 @@ class ObjectType implements TypeWithClassName, SubtractableType /** @var array>> */ private static array $properties = []; + /** @var array>> */ + private static array $instanceProperties = []; + + /** @var array>> */ + private static array $staticProperties = []; + /** @var array> */ private static array $ancestors = []; @@ -132,6 +138,8 @@ public static function resetCaches(): void self::$superTypes = []; self::$methods = []; self::$properties = []; + self::$instanceProperties = []; + self::$staticProperties = []; self::$ancestors = []; self::$enumCases = []; } @@ -247,6 +255,172 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return TrinaryLogic::createMaybe(); + } + + if ($classReflection->hasInstanceProperty($propertyName)) { + return TrinaryLogic::createYes(); + } + + if ($classReflection->allowsDynamicProperties()) { + return TrinaryLogic::createMaybe(); + } + + if (!$classReflection->isFinal()) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + if (!$scope->isInClass()) { + $canAccessProperty = 'no'; + } else { + $canAccessProperty = $scope->getClassReflection()->getName(); + } + $description = $this->describeCache(); + + if (isset(self::$instanceProperties[$description][$propertyName][$canAccessProperty])) { + return self::$instanceProperties[$description][$propertyName][$canAccessProperty]; + } + + $nakedClassReflection = $this->getNakedClassReflection(); + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); + } + + if ($nakedClassReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($propertyName === 'value' && $nakedClassReflection->isBackedEnum()) + ) { + $properties = []; + foreach ($this->getEnumCases() as $enumCase) { + $properties[] = $enumCase->getUnresolvedPropertyPrototype($propertyName, $scope); + } + + if (count($properties) > 0) { + if (count($properties) === 1) { + return $properties[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $properties); + } + } + } + + if (!$nakedClassReflection->hasNativeProperty($propertyName)) { + $nakedClassReflection = $this->getClassReflection(); + } + + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); + } + + $property = $nakedClassReflection->getInstanceProperty($propertyName, $scope); + + $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); + $resolvedClassReflection = null; + if ($ancestor !== null && $ancestor->hasInstanceProperty($propertyName)->yes()) { + $resolvedClassReflection = $ancestor->getClassReflection(); + if ($ancestor !== $this) { + $property = $ancestor->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getNakedProperty(); + } + } + if ($resolvedClassReflection === null) { + $resolvedClassReflection = $property->getDeclaringClass(); + } + + return self::$instanceProperties[$description][$propertyName][$canAccessProperty] = new CalledOnTypeUnresolvedPropertyPrototypeReflection( + $property, + $resolvedClassReflection, + true, + $this, + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return TrinaryLogic::createMaybe(); + } + + if ($classReflection->hasStaticProperty($propertyName)) { + return TrinaryLogic::createYes(); + } + + if (!$classReflection->isFinal()) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + if (!$scope->isInClass()) { + $canAccessProperty = 'no'; + } else { + $canAccessProperty = $scope->getClassReflection()->getName(); + } + $description = $this->describeCache(); + + if (isset(self::$staticProperties[$description][$propertyName][$canAccessProperty])) { + return self::$staticProperties[$description][$propertyName][$canAccessProperty]; + } + + $nakedClassReflection = $this->getNakedClassReflection(); + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); + } + + if (!$nakedClassReflection->hasNativeProperty($propertyName)) { + $nakedClassReflection = $this->getClassReflection(); + } + + if ($nakedClassReflection === null) { + throw new ClassNotFoundException($this->className); + } + + $property = $nakedClassReflection->getStaticProperty($propertyName, $scope); + + $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); + $resolvedClassReflection = null; + if ($ancestor !== null && $ancestor->hasStaticProperty($propertyName)->yes()) { + $resolvedClassReflection = $ancestor->getClassReflection(); + if ($ancestor !== $this) { + $property = $ancestor->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getNakedProperty(); + } + } + if ($resolvedClassReflection === null) { + $resolvedClassReflection = $property->getDeclaringClass(); + } + + return self::$staticProperties[$description][$propertyName][$canAccessProperty] = new CalledOnTypeUnresolvedPropertyPrototypeReflection( + $property, + $resolvedClassReflection, + true, + $this, + ); + } + /** * @deprecated Not in use anymore. */ diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 2d5a7c3ad8..1f659eb4f5 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -236,6 +236,70 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->getStaticObjectType()->hasInstanceProperty($propertyName); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $staticObject = $this->getStaticObjectType(); + $nakedProperty = $staticObject->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getNakedProperty(); + + $ancestor = $this->getAncestorWithClassName($nakedProperty->getDeclaringClass()->getName()); + $classReflection = null; + if ($ancestor !== null) { + $classReflection = $ancestor->getClassReflection(); + } + if ($classReflection === null) { + $classReflection = $nakedProperty->getDeclaringClass(); + } + + return new CallbackUnresolvedPropertyPrototypeReflection( + $nakedProperty, + $classReflection, + false, + fn (Type $type): Type => $this->transformStaticType($type, $scope), + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->getStaticObjectType()->hasStaticProperty($propertyName); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $staticObject = $this->getStaticObjectType(); + $nakedProperty = $staticObject->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getNakedProperty(); + + $ancestor = $this->getAncestorWithClassName($nakedProperty->getDeclaringClass()->getName()); + $classReflection = null; + if ($ancestor !== null) { + $classReflection = $ancestor->getClassReflection(); + } + if ($classReflection === null) { + $classReflection = $nakedProperty->getDeclaringClass(); + } + + return new CallbackUnresolvedPropertyPrototypeReflection( + $nakedProperty, + $classReflection, + false, + fn (Type $type): Type => $this->transformStaticType($type, $scope), + ); + } + public function canCallMethods(): TrinaryLogic { return $this->getStaticObjectType()->canCallMethods(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 0ff25dc124..71678b77a4 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -135,6 +135,36 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember throw new ShouldNotHappenException(); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + public function canCallMethods(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 0e1dee9904..e4c6d1f597 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -113,6 +113,36 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return $this->resolve()->getUnresolvedPropertyPrototype($propertyName, $scope); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->resolve()->hasInstanceProperty($propertyName); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->resolve()->getInstanceProperty($propertyName, $scope); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->resolve()->getUnresolvedInstancePropertyPrototype($propertyName, $scope); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->resolve()->hasStaticProperty($propertyName); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->resolve()->getStaticProperty($propertyName, $scope); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + return $this->resolve()->getUnresolvedPropertyPrototype($propertyName, $scope); + } + public function canCallMethods(): TrinaryLogic { return $this->resolve()->canCallMethods(); diff --git a/src/Type/Traits/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index 4625da358b..af7effb96a 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -61,6 +61,48 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection(); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection(); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + public function canCallMethods(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Traits/NonObjectTypeTrait.php b/src/Type/Traits/NonObjectTypeTrait.php index d16b86c9b1..0d55341b46 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -46,6 +46,36 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember throw new ShouldNotHappenException(); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + public function canCallMethods(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index d8a52c200d..4e93749d30 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -72,6 +72,48 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection(); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $property = new DummyPropertyReflection(); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + public function canCallMethods(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 571c2a2bf2..19e474f9e3 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -74,12 +74,27 @@ public function describe(VerbosityLevel $level): string; public function canAccessProperties(): TrinaryLogic; + /** @deprecated Use hasInstanceProperty or hasStaticProperty instead */ public function hasProperty(string $propertyName): TrinaryLogic; + /** @deprecated Use getInstanceProperty or getStaticProperty instead */ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; + /** @deprecated Use getUnresolvedInstancePropertyPrototype or getUnresolvedStaticPropertyPrototype instead */ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection; + public function hasInstanceProperty(string $propertyName): TrinaryLogic; + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection; + + public function hasStaticProperty(string $propertyName): TrinaryLogic; + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection; + public function canCallMethods(): TrinaryLogic; public function hasMethod(string $methodName): TrinaryLogic; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 6661e07d57..5b4966ea30 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -493,6 +493,72 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName)); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasInstanceProperty($propertyName)->yes()) { + continue; + } + + $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new ShouldNotHappenException(); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName)); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + $propertyPrototypes = []; + foreach ($this->types as $type) { + if (!$type->hasStaticProperty($propertyName)->yes()) { + continue; + } + + $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this); + } + + $propertiesCount = count($propertyPrototypes); + if ($propertiesCount === 0) { + throw new ShouldNotHappenException(); + } + + if ($propertiesCount === 1) { + return $propertyPrototypes[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes); + } + public function canCallMethods(): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods()); diff --git a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php index 6ded7325fb..44204e0f2c 100644 --- a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php +++ b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php @@ -98,6 +98,36 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember // TODO: Implement getUnresolvedPropertyPrototype() method. } + public function hasInstanceProperty(string $propertyName): \PHPStan\TrinaryLogic + { + // TODO: Implement hasInstanceProperty() method. + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): \PHPStan\Reflection\PropertyReflection + { + // TODO: Implement getInstanceProperty() method. + } + + public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): \PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection + { + // TODO: Implement getUnresolvedInstancePropertyPrototype() method. + } + + public function hasStaticProperty(string $propertyName): \PHPStan\TrinaryLogic + { + // TODO: Implement hasStaticProperty() method. + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): \PHPStan\Reflection\PropertyReflection + { + // TODO: Implement getStaticProperty() method. + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): \PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection + { + // TODO: Implement getUnresolvedStaticPropertyPrototype() method. + } + public function canCallMethods(): \PHPStan\TrinaryLogic { // TODO: Implement canCallMethods() method. From 5fd35271d29dbe2946a5add6408125bbbac0bbe1 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 28 Mar 2025 14:42:00 +0100 Subject: [PATCH 04/27] Solve tests --- tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index 2b0022ce56..7d3246d014 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -42,22 +42,22 @@ public function testRuleOutOfPhpStan(): void ], [ 'Implementing PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 333, + 363, $tip, ], [ 'Implementing PHPStan\Analyser\Scope is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 338, + 368, $tip, ], [ 'Implementing PHPStan\Reflection\FunctionReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 343, + 373, $tip, ], [ 'Implementing PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 347, + 377, $tip, ], ]); From f699b9e7d735dca78555d65e7417e85e84d685d9 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 29 Mar 2025 10:32:02 +0100 Subject: [PATCH 05/27] More --- ...endsPropertiesClassReflectionExtension.php | 24 ++++------- .../Properties/AccessPropertiesCheck.php | 29 ++++++------- .../Properties/AccessStaticPropertiesRule.php | 41 +++++++++---------- src/Type/Accessory/HasPropertyType.php | 22 ++++++++++ src/Type/ObjectShapeType.php | 17 -------- src/Type/Traits/ObjectTypeTrait.php | 4 +- .../PHPStan/Type/BenevolentUnionTypeTest.php | 8 ++-- 7 files changed, 68 insertions(+), 77 deletions(-) diff --git a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php index a0615b82f2..27b51f7fdd 100644 --- a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php +++ b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php @@ -44,10 +44,8 @@ public function hasInstanceProperty(ClassReflection $classReflection, string $pr return $this->findProperty( $classReflection, $propertyName, - // TODO: Use hasInstanceProperty - static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), - // TODO: Use getInstanceProperty - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasInstanceProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()) ) !== null; } @@ -56,10 +54,8 @@ public function getInstanceProperty(ClassReflection $classReflection, string $pr $property = $this->findProperty( $classReflection, $propertyName, - // TODO: Use hasInstanceProperty - static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), - // TODO: Use getInstanceProperty - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasInstanceProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()) ); if ($property === null) { throw new ShouldNotHappenException(); @@ -73,10 +69,8 @@ public function hasStaticProperty(ClassReflection $classReflection, string $prop return $this->findProperty( $classReflection, $propertyName, - // TODO: Use hasStaticProperty - static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), - // TODO: Use getStaticProperty - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasStaticProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()) ) !== null; } @@ -85,10 +79,8 @@ public function getStaticProperty(ClassReflection $classReflection, string $prop $property = $this->findProperty( $classReflection, $propertyName, - // TODO: Use hasStaticProperty - static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), - // TODO: Use getStaticProperty - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasStaticProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()) ); if ($property === null) { throw new ShouldNotHappenException(); diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index f45fa4c598..15daac4da2 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -101,8 +101,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $scope, NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), - // TODO use hasInstanceProperty - static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(), + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasInstanceProperty($name)->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -128,8 +127,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ]; } - // TODO use hasInstanceProperty - $has = $type->hasProperty($name); + $has = $type->hasInstanceProperty($name); if ($has->maybe()) { if ($scope->isUndefinedExpressionAllowed($node)) { if (!$this->checkDynamicProperties) { @@ -202,6 +200,16 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string } } + if ($type->hasStaticProperty($name)->yes()) { + return [ + RuleErrorBuilder::message(sprintf( + 'Non-static access to static property %s::$%s.', + $type->getStaticProperty($name, $scope)->getDeclaringClass()->getDisplayName(), + $name, + ))->identifier('staticProperty.nonStaticAccess')->build(), + ]; + } + $ruleErrorBuilder = RuleErrorBuilder::message(sprintf( 'Access to an undefined property %s::$%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), @@ -218,18 +226,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ]; } - // TODO use getInstanceProperty - $propertyReflection = $type->getProperty($name, $scope); - if ($propertyReflection->isStatic()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Non-static access to static property %s::$%s.', - $propertyReflection->getDeclaringClass()->getDisplayName(), - $name, - ))->identifier('staticProperty.nonStaticAccess')->build(), - ]; - } - + $propertyReflection = $type->getInstanceProperty($name, $scope); if ($write) { if ($scope->canWriteProperty($propertyReflection)) { return []; diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index b845f7970d..0f205ea7f9 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -155,8 +155,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $scope, NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class), sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), - // TODO Use hasStaticProperty - static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(), + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasStaticProperty($name)->yes(), ); $classType = $classTypeResult->getType(); if ($classType instanceof ErrorType) { @@ -188,8 +187,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]); } - // TODO Use hasStaticProperty - $has = $classType->hasProperty($name); + $has = $classType->hasStaticProperty($name); if (!$has->no() && $scope->isUndefinedExpressionAllowed($node)) { return []; } @@ -222,6 +220,23 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, } } + if ($classType->hasInstanceProperty($name)->yes()) { + $hasPropertyTypes = TypeUtils::getHasPropertyTypes($classType); + foreach ($hasPropertyTypes as $hasPropertyType) { + if ($hasPropertyType->getPropertyName() === $name) { + return []; + } + } + + return array_merge($messages, [ + RuleErrorBuilder::message(sprintf( + 'Static access to instance property %s::$%s.', + $classType->getInstanceProperty($name, $scope)->getDeclaringClass()->getDisplayName(), + $name, + ))->identifier('property.staticAccess')->build(), + ]); + } + return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Access to an undefined static property %s::$%s.', @@ -231,25 +246,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]); } - // TODO Use getStaticProperty and update the if $property = $classType->getProperty($name, $scope); - if (!$property->isStatic()) { - $hasPropertyTypes = TypeUtils::getHasPropertyTypes($classType); - foreach ($hasPropertyTypes as $hasPropertyType) { - if ($hasPropertyType->getPropertyName() === $name) { - return []; - } - } - - return array_merge($messages, [ - RuleErrorBuilder::message(sprintf( - 'Static access to instance property %s::$%s.', - $property->getDeclaringClass()->getDisplayName(), - $name, - ))->identifier('property.staticAccess')->build(), - ]); - } - if (!$scope->canReadProperty($property)) { return array_merge($messages, [ RuleErrorBuilder::message(sprintf( diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 0c2ca8e47a..c6dd2be824 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -72,6 +72,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + // TODO return new IsSuperTypeOfResult($type->hasProperty($this->propertyName), []); } @@ -87,6 +88,7 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult $limit = IsSuperTypeOfResult::createMaybe(); } + // TODO return $limit->and(new IsSuperTypeOfResult($otherType->hasProperty($this->propertyName), [])); } @@ -115,6 +117,26 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } + // TODO + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + if ($this->propertyName === $propertyName) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createMaybe(); + } + + // TODO + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + if ($this->propertyName === $propertyName) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createMaybe(); + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [new TrivialParametersAcceptor()]; diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index 6677f4ffa9..ebafb8e8a5 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -137,23 +137,6 @@ public function getUnresolvedInstancePropertyPrototype(string $propertyName, Cla ); } - public function hasStaticProperty(string $propertyName): TrinaryLogic - { - // TODO Change the implementation - return $this->hasInstanceProperty($propertyName); - } - - public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection - { - return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); - } - - public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection - { - // TODO Change the implementation - return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope); - } - public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 4e93749d30..84e16c32ea 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -79,7 +79,7 @@ public function hasInstanceProperty(string $propertyName): TrinaryLogic public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { - return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection @@ -100,7 +100,7 @@ public function hasStaticProperty(string $propertyName): TrinaryLogic public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { - return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection diff --git a/tests/PHPStan/Type/BenevolentUnionTypeTest.php b/tests/PHPStan/Type/BenevolentUnionTypeTest.php index 92ce5521cb..8749301bc7 100644 --- a/tests/PHPStan/Type/BenevolentUnionTypeTest.php +++ b/tests/PHPStan/Type/BenevolentUnionTypeTest.php @@ -55,7 +55,7 @@ public function testCanAccessProperties(BenevolentUnionType $type, TrinaryLogic /** * @return Iterator */ - public static function dataHasProperty(): Iterator + public static function dataHasInstanceProperty(): Iterator { yield [ new BenevolentUnionType([ @@ -82,10 +82,10 @@ public static function dataHasProperty(): Iterator ]; } - #[DataProvider('dataHasProperty')] - public function testHasProperty(BenevolentUnionType $type, string $propertyName, TrinaryLogic $expectedResult): void + #[DataProvider('dataHasInstanceProperty')] + public function testHasInstanceProperty(BenevolentUnionType $type, string $propertyName, TrinaryLogic $expectedResult): void { - $actualResult = $type->hasProperty($propertyName); + $actualResult = $type->hasInstanceProperty($propertyName); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), From e3b996960420708dc0d415f121ebcb63525df94b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 29 Mar 2025 12:07:55 +0100 Subject: [PATCH 06/27] Solve deprecations --- src/Analyser/MutatingScope.php | 9 ++++++-- src/Analyser/NodeScopeResolver.php | 8 +++---- src/Node/ClassPropertiesNode.php | 2 +- ...ixinPropertiesClassReflectionExtension.php | 4 ++-- ...AccessPrivatePropertyThroughStaticRule.php | 7 ++---- .../Properties/AccessStaticPropertiesRule.php | 2 +- src/Type/IntersectionType.php | 2 +- src/Type/ObjectShapeType.php | 17 +++++++------- src/Type/ObjectType.php | 23 +------------------ ...nPropertyConstructorThrowTypeExtension.php | 2 +- src/Type/Traits/LateResolvableTypeTrait.php | 2 +- src/Type/Traits/MaybeObjectTypeTrait.php | 4 ++-- 12 files changed, 32 insertions(+), 50 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index fae28b1e26..411ad6faee 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4370,7 +4370,7 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN return $this; } - $propertyReflection = $this->getPropertyReflection($fetchedOnType, $propertyName); + $propertyReflection = $this->getInstancePropertyReflection($fetchedOnType, $propertyName); if ($propertyReflection === null) { return $this; } @@ -6251,7 +6251,12 @@ public function getStaticPropertyReflection(Type $typeWithProperty, string $prop */ private function propertyFetchType(Type $fetchedOnType, string $propertyName, Expr $propertyFetch): ?Type { - $propertyReflection = $this->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyFetch instanceof PropertyFetch) { + $propertyReflection = $this->getInstancePropertyReflection($fetchedOnType, $propertyName); + } else { + $propertyReflection = $this->getStaticPropertyReflection($fetchedOnType, $propertyName); + } + if ($propertyReflection === null) { return null; } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 0fbac39d6b..f71c217b11 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3216,7 +3216,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { } else { $propertyName = $expr->name->toString(); $propertyHolderType = $scopeBeforeVar->getType($expr->var); - $propertyReflection = $scopeBeforeVar->getPropertyReflection($propertyHolderType, $propertyName); + $propertyReflection = $scopeBeforeVar->getInstancePropertyReflection($propertyHolderType, $propertyName); if ($propertyReflection !== null && $this->phpVersion->supportsPropertyHooks()) { $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); if ($propertyDeclaringClass->hasNativeProperty($propertyName)) { @@ -5898,8 +5898,8 @@ static function (): void { } $propertyHolderType = $scope->getType($var->var); - if ($propertyName !== null && $propertyHolderType->hasProperty($propertyName)->yes()) { - $propertyReflection = $propertyHolderType->getProperty($propertyName, $scope); + if ($propertyName !== null && $propertyHolderType->hasInstanceProperty($propertyName)->yes()) { + $propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope); $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); if ($propertyReflection->canChangeTypeAfterAssignment()) { @@ -5991,7 +5991,7 @@ static function (): void { $scope = $result->getScope(); if ($propertyName !== null) { - $propertyReflection = $scope->getPropertyReflection($propertyHolderType, $propertyName); + $propertyReflection = $scope->getStaticPropertyReflection($propertyHolderType, $propertyName); $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index 1a538de5d1..5f47c928b5 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -201,7 +201,7 @@ public function getUninitializedProperties( continue; } - $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName); + $propertyReflection = $usageScope->getInstancePropertyReflection($fetchedOnType, $propertyName); if ($propertyReflection === null) { continue; } diff --git a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php index 6a2cfe4b77..b999c1cf61 100644 --- a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php @@ -54,12 +54,12 @@ private function findProperty(ClassReflection $classReflection, string $property $this->inProcess[$typeDescription][$propertyName] = true; - if (!$type->hasProperty($propertyName)->yes()) { + if (!$type->hasInstanceProperty($propertyName)->yes()) { unset($this->inProcess[$typeDescription][$propertyName]); continue; } - $property = $type->getProperty($propertyName, new OutOfClassScope()); + $property = $type->getInstanceProperty($propertyName, new OutOfClassScope()); unset($this->inProcess[$typeDescription][$propertyName]); return $property; diff --git a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php index 70efb0dab4..44773b9275 100644 --- a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -38,17 +38,14 @@ public function processNode(Node $node, Scope $scope): array } $classType = $scope->resolveTypeByName($className); - if (!$classType->hasProperty($propertyName)->yes()) { + if (!$classType->hasStaticProperty($propertyName)->yes()) { return []; } - $property = $classType->getProperty($propertyName, $scope); + $property = $classType->getStaticProperty($propertyName, $scope); if (!$property->isPrivate()) { return []; } - if (!$property->isStatic()) { - return []; - } if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { return []; diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 0f205ea7f9..6073c101dd 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -246,7 +246,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]); } - $property = $classType->getProperty($name, $scope); + $property = $classType->getStaticProperty($name, $scope); if (!$scope->canReadProperty($property)) { return array_merge($messages, [ RuleErrorBuilder::message(sprintf( diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index d643f66c18..eea44a41b4 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -552,7 +552,7 @@ public function getUnresolvedInstancePropertyPrototype(string $propertyName, Cla { $propertyPrototypes = []; foreach ($this->types as $type) { - if (!$type->hasProperty($propertyName)->yes()) { + if (!$type->hasInstanceProperty($propertyName)->yes()) { continue; } diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index ebafb8e8a5..4e953666a5 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -158,7 +158,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult $result = AcceptsResult::createYes(); $scope = new OutOfClassScope(); foreach ($this->properties as $propertyName => $propertyType) { - $typeHasProperty = $type->hasProperty((string) $propertyName); + $typeHasProperty = $type->hasInstanceProperty((string) $propertyName); $hasProperty = new AcceptsResult( $typeHasProperty, $typeHasProperty->yes() ? [] : [ @@ -189,7 +189,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult $result = $result->and($hasProperty); try { - $otherProperty = $type->getProperty((string) $propertyName, $scope); + $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope); } catch (MissingPropertyFromReflectionException) { return AcceptsResult::createNo( [ @@ -275,7 +275,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult $result = IsSuperTypeOfResult::createYes(); $scope = new OutOfClassScope(); foreach ($this->properties as $propertyName => $propertyType) { - $hasProperty = new IsSuperTypeOfResult($type->hasProperty((string) $propertyName), []); + $hasProperty = new IsSuperTypeOfResult($type->hasInstanceProperty((string)$propertyName), []); if ($hasProperty->no()) { if (in_array($propertyName, $this->optionalProperties, true)) { continue; @@ -294,7 +294,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult $result = $result->and($hasProperty); try { - $otherProperty = $type->getProperty((string) $propertyName, $scope); + $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope); } catch (MissingPropertyFromReflectionException) { return IsSuperTypeOfResult::createNo( [ @@ -306,6 +306,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult ], ); } + if (!$otherProperty->isPublic()) { return IsSuperTypeOfResult::createNo(); } @@ -398,12 +399,12 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $typeMap = TemplateTypeMap::createEmpty(); $scope = new OutOfClassScope(); foreach ($this->properties as $name => $propertyType) { - if ($receivedType->hasProperty((string) $name)->no()) { + if ($receivedType->hasInstanceProperty((string) $name)->no()) { continue; } try { - $receivedProperty = $receivedType->getProperty((string) $name, $scope); + $receivedProperty = $receivedType->getInstanceProperty((string) $name, $scope); } catch (MissingPropertyFromReflectionException) { continue; } @@ -489,10 +490,10 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $scope = new OutOfClassScope(); foreach ($this->properties as $name => $propertyType) { - if (!$right->hasProperty((string) $name)->yes()) { + if (!$right->hasInstanceProperty((string) $name)->yes()) { return $this; } - $transformed = $cb($propertyType, $right->getProperty((string) $name, $scope)->getReadableType()); + $transformed = $cb($propertyType, $right->getInstanceProperty((string) $name, $scope)->getReadableType()); if ($transformed !== $propertyType) { $stillOriginal = false; } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 558f1fa9f5..d3e1bb398c 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -307,7 +307,7 @@ public function getUnresolvedInstancePropertyPrototype(string $propertyName, Cla ) { $properties = []; foreach ($this->getEnumCases() as $enumCase) { - $properties[] = $enumCase->getUnresolvedPropertyPrototype($propertyName, $scope); + $properties[] = $enumCase->getUnresolvedInstancePropertyPrototype($propertyName, $scope); } if (count($properties) > 0) { @@ -421,27 +421,6 @@ public function getUnresolvedStaticPropertyPrototype(string $propertyName, Class ); } - /** - * @deprecated Not in use anymore. - */ - public function getPropertyWithoutTransformingStatic(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection - { - $classReflection = $this->getNakedClassReflection(); - if ($classReflection === null) { - throw new ClassNotFoundException($this->className); - } - - if (!$classReflection->hasProperty($propertyName)) { - $classReflection = $this->getClassReflection(); - } - - if ($classReflection === null) { - throw new ClassNotFoundException($this->className); - } - - return $classReflection->getProperty($propertyName, $scope); - } - public function getReferencedClasses(): array { return [$this->className]; diff --git a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php index 22140eba68..64392e86a1 100644 --- a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php @@ -42,7 +42,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $classReflection = $this->reflectionProvider->getClass($constantString->getValue()); foreach ($propertyType->getConstantStrings() as $constantPropertyString) { - if (!$classReflection->hasProperty($constantPropertyString->getValue())) { + if (!$classReflection->hasInstanceProperty($constantPropertyString->getValue())) { return $methodReflection->getThrowType(); } } diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index e4c6d1f597..6377fb21eb 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -140,7 +140,7 @@ public function getStaticProperty(string $propertyName, ClassMemberAccessAnswere public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - return $this->resolve()->getUnresolvedPropertyPrototype($propertyName, $scope); + return $this->resolve()->getUnresolvedStaticPropertyPrototype($propertyName, $scope); } public function canCallMethods(): TrinaryLogic diff --git a/src/Type/Traits/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index af7effb96a..d919a587f4 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -68,7 +68,7 @@ public function hasInstanceProperty(string $propertyName): TrinaryLogic public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { - return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection @@ -89,7 +89,7 @@ public function hasStaticProperty(string $propertyName): TrinaryLogic public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection { - return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection From db5d34430cbccb9d1b1dd306e076d3686d78adb1 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 29 Mar 2025 12:31:12 +0100 Subject: [PATCH 07/27] Fix tests --- src/Type/ObjectShapeType.php | 6 ++++++ tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 4 ---- .../Analyser/nsrt/bug-nullsafe-prop-static-access.php | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index 4e953666a5..cc0aebf201 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -187,6 +187,12 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult $hasProperty = AcceptsResult::createYes(); } + if (!$hasProperty->yes() && $type->hasStaticProperty($propertyName)->yes()) { + return new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is static.', $type->getStaticProperty($propertyName, $scope)->getDeclaringClass()->getDisplayName(), $propertyName), + ]); + } + $result = $result->and($hasProperty); try { $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope); diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 7e72362f04..dd6500583e 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8749,10 +8749,6 @@ public static function dataUnionProperties(): array 'UnionProperties\Bar|UnionProperties\Foo', '$something->doSomething', ], - [ - 'UnionProperties\Bar|UnionProperties\Foo', - '$something::$doSomething', - ], ]; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php b/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php index 14d4ecf708..82639be3e9 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php +++ b/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php @@ -26,5 +26,4 @@ function foo(?A $a): void \PHPStan\Testing\assertType('string|null', $a?->b->get()); \PHPStan\Testing\assertType('int|null', $a?->b::$value); - \PHPStan\Testing\assertType('int|null', $a?->b->value); } From 9aa91a69ffb11e602d95a105debc9b3869dbb352 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 29 Mar 2025 12:46:43 +0100 Subject: [PATCH 08/27] Solve deprecations --- .../InitializerExprTypeResolver.php | 4 +-- .../DeadCode/UnusedPrivatePropertyRule.php | 9 ++++- .../Properties/PropertyReflectionFinder.php | 33 ++++++++++++++----- src/Type/Php/ArrayColumnHelper.php | 4 +-- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 49e225d201..8b38c5a383 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -408,11 +408,11 @@ public function getType(Expr $expr, InitializerExprContext $context): Type if ($expr instanceof PropertyFetch && $expr->name instanceof Identifier) { $fetchedOnType = $this->getType($expr->var, $context); - if (!$fetchedOnType->hasProperty($expr->name->name)->yes()) { + if (!$fetchedOnType->hasInstanceProperty($expr->name->name)->yes()) { return new ErrorType(); } - return $fetchedOnType->getProperty($expr->name->name, new OutOfClassScope())->getReadableType(); + return $fetchedOnType->getInstanceProperty($expr->name->name, new OutOfClassScope())->getReadableType(); } return new MixedType(); diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index c6bb859d6d..7d2ccc7328 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -182,7 +182,14 @@ public function processNode(Node $node, Scope $scope): array if (!array_key_exists($propertyName, $properties)) { continue; } - $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName); + + $propertyNode = $properties[$propertyName]['node']; + if ($propertyNode->isStatic()) { + $propertyReflection = $usageScope->getStaticPropertyReflection($fetchedOnType, $propertyName); + } else { + $propertyReflection = $usageScope->getInstancePropertyReflection($fetchedOnType, $propertyName); + } + if ($propertyReflection === null) { if (!$classType->isSuperTypeOf($fetchedOnType)->no()) { if ($usage instanceof PropertyRead) { diff --git a/src/Rules/Properties/PropertyReflectionFinder.php b/src/Rules/Properties/PropertyReflectionFinder.php index 3daa5ee867..b25682687b 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -33,7 +33,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a $reflections = []; $propertyHolderType = $scope->getType($propertyFetch->var); foreach ($names as $name) { - $reflection = $this->findPropertyReflection( + $reflection = $this->findInstancePropertyReflection( $propertyHolderType, $name, $propertyFetch->name instanceof Expr ? $scope->filterByTruthyValue(new Expr\BinaryOp\Identical( @@ -65,7 +65,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a $reflections = []; foreach ($names as $name) { - $reflection = $this->findPropertyReflection( + $reflection = $this->findStaticPropertyReflection( $propertyHolderType, $name, $propertyFetch->name instanceof Expr ? $scope->filterByTruthyValue(new Expr\BinaryOp\Identical( @@ -91,13 +91,13 @@ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?F if ($propertyFetch instanceof Node\Expr\PropertyFetch) { $propertyHolderType = $scope->getType($propertyFetch->var); if ($propertyFetch->name instanceof Node\Identifier) { - return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + return $this->findInstancePropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); } $nameType = $scope->getType($propertyFetch->name); $nameTypeConstantStrings = $nameType->getConstantStrings(); if (count($nameTypeConstantStrings) === 1) { - return $this->findPropertyReflection($propertyHolderType, $nameTypeConstantStrings[0]->getValue(), $scope); + return $this->findInstancePropertyReflection($propertyHolderType, $nameTypeConstantStrings[0]->getValue(), $scope); } return null; @@ -113,16 +113,33 @@ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?F $propertyHolderType = $scope->getType($propertyFetch->class); } - return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + return $this->findStaticPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); } - private function findPropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection + private function findInstancePropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection { - if (!$propertyHolderType->hasProperty($propertyName)->yes()) { + if (!$propertyHolderType->hasInstanceProperty($propertyName)->yes()) { return null; } - $originalProperty = $propertyHolderType->getProperty($propertyName, $scope); + $originalProperty = $propertyHolderType->getInstanceProperty($propertyName, $scope); + + return new FoundPropertyReflection( + $originalProperty, + $scope, + $propertyName, + $originalProperty->getReadableType(), + $originalProperty->getWritableType(), + ); + } + + private function findStaticPropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection + { + if (!$propertyHolderType->hasStaticProperty($propertyName)->yes()) { + return null; + } + + $originalProperty = $propertyHolderType->getStaticProperty($propertyName, $scope); return new FoundPropertyReflection( $originalProperty, diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php index d1d40e8cd2..179d85bc04 100644 --- a/src/Type/Php/ArrayColumnHelper.php +++ b/src/Type/Php/ArrayColumnHelper.php @@ -139,7 +139,7 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $ } foreach ($propertyTypes as $propertyType) { $propertyName = $propertyType->getValue(); - $hasProperty = $type->hasProperty($propertyName); + $hasProperty = $type->hasInstanceProperty($propertyName); if ($hasProperty->maybe()) { return [new MixedType(), TrinaryLogic::createMaybe()]; } @@ -147,7 +147,7 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $ continue; } - $returnTypes[] = $type->getProperty($propertyName, $scope)->getReadableType(); + $returnTypes[] = $type->getInstanceProperty($propertyName, $scope)->getReadableType(); } } From 84794792abc5d41775a57eb6df71faab032c700a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 29 Mar 2025 13:14:35 +0100 Subject: [PATCH 09/27] Fix test --- src/Type/ObjectType.php | 1 - .../Rules/Properties/TypesAssignedToPropertiesRuleTest.php | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index d3e1bb398c..4aea9fa21c 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -23,7 +23,6 @@ use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index d9916e1e13..de2b53682c 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -45,10 +45,6 @@ public function testTypesAssignedToProperties(): void 'Static property PropertiesAssignedTypes\Foo::$staticStringProperty (string) does not accept int.', 37, ], - [ - 'Property PropertiesAssignedTypes\Ipsum::$parentStringProperty (string) does not accept int.', - 39, - ], [ 'Property PropertiesAssignedTypes\Foo::$unionPropertySelf (array|(iterable&PropertiesAssignedTypes\Collection)) does not accept PropertiesAssignedTypes\Foo.', 44, From 7878c0c3c7c6a62b21e42c4676df472a0db41521 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 29 Mar 2025 13:21:38 +0100 Subject: [PATCH 10/27] Solve deprecation --- src/Dependency/DependencyResolver.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index c487355dcf..9a77ca357b 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -261,7 +261,7 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } if ($node->name instanceof Node\Identifier) { - $propertyReflection = $scope->getPropertyReflection($fetchedOnType, $node->name->toString()); + $propertyReflection = $scope->getInstancePropertyReflection($fetchedOnType, $node->name->toString()); if ($propertyReflection !== null) { $this->addClassToDependencies($propertyReflection->getDeclaringClass()->getName(), $dependenciesReflections); } @@ -377,13 +377,13 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $className = $scope->resolveName($node->class); if ($this->reflectionProvider->hasClass($className)) { $propertyClassReflection = $this->reflectionProvider->getClass($className); - if ($propertyClassReflection->hasProperty($node->name->toString())) { - $propertyReflection = $propertyClassReflection->getProperty($node->name->toString(), $scope); + if ($propertyClassReflection->hasStaticProperty($node->name->toString())) { + $propertyReflection = $propertyClassReflection->getStaticProperty($node->name->toString(), $scope); $this->addClassToDependencies($propertyReflection->getDeclaringClass()->getName(), $dependenciesReflections); } } } else { - $propertyReflection = $scope->getPropertyReflection($scope->getType($node->class), $node->name->toString()); + $propertyReflection = $scope->getStaticPropertyReflection($scope->getType($node->class), $node->name->toString()); if ($propertyReflection !== null) { $this->addClassToDependencies($propertyReflection->getDeclaringClass()->getName(), $dependenciesReflections); } From 5beb7961ea3acb9db8e4d163e872047e0ee793e7 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 29 Mar 2025 13:56:58 +0100 Subject: [PATCH 11/27] Add non regression test --- .../AccessStaticPropertiesRuleTest.php | 5 +++++ .../PHPStan/Rules/Properties/data/bug-12775.php | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-12775.php diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index deba1aeb7c..785e833194 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -312,4 +312,9 @@ public function testBug8333(): void ]); } + public function testBug12775(): void + { + $this->analyse([__DIR__ . '/data/bug-12775.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-12775.php b/tests/PHPStan/Rules/Properties/data/bug-12775.php new file mode 100644 index 0000000000..bdade366f3 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12775.php @@ -0,0 +1,17 @@ + Date: Sat, 29 Mar 2025 14:23:25 +0100 Subject: [PATCH 12/27] Review --- src/Dependency/DependencyResolver.php | 2 +- src/Reflection/ClassReflection.php | 31 ++++++------------- .../Properties/AccessStaticPropertiesRule.php | 2 +- src/Type/ObjectType.php | 2 +- .../Annotations/DeprecatedAnnotationsTest.php | 2 +- .../Annotations/InternalAnnotationsTest.php | 2 +- 6 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 9a77ca357b..95a9484fae 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -378,7 +378,7 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies if ($this->reflectionProvider->hasClass($className)) { $propertyClassReflection = $this->reflectionProvider->getClass($className); if ($propertyClassReflection->hasStaticProperty($node->name->toString())) { - $propertyReflection = $propertyClassReflection->getStaticProperty($node->name->toString(), $scope); + $propertyReflection = $propertyClassReflection->getStaticProperty($node->name->toString()); $this->addClassToDependencies($propertyReflection->getDeclaringClass()->getName(), $dependenciesReflections); } } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index eef4e9860d..56c249fb27 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -802,35 +802,24 @@ public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswe return $this->instanceProperties[$key]; } - public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + public function getStaticProperty(string $propertyName): ExtendedPropertyReflection { $key = $propertyName; - if ($scope->isInClass()) { - $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey()); - } - - if (!isset($this->staticProperties[$key])) { - if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { - $property = $this->wrapExtendedProperty($this->getPhpExtension()->getStaticProperty($this, $propertyName)); - if ($scope->canReadProperty($property)) { - return $this->staticProperties[$key] = $property; - } - $this->staticProperties[$key] = $property; - } + if (isset($this->staticProperties[$key])) { + return $this->staticProperties[$key]; } - if (!isset($this->staticProperties[$key])) { - if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { - $property = $this->requireExtendsPropertiesClassReflectionExtension->getStaticProperty($this, $propertyName); - $this->staticProperties[$key] = $property; - } + if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { + $property = $this->wrapExtendedProperty($this->getPhpExtension()->getStaticProperty($this, $propertyName)); + return $this->staticProperties[$key] = $property; } - if (!isset($this->staticProperties[$key])) { - throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); + if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getStaticProperty($this, $propertyName); + return $this->staticProperties[$key] = $property; } - return $this->staticProperties[$key]; + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); } public function hasNativeProperty(string $propertyName): bool diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 6073c101dd..19caf0e08b 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -204,7 +204,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, while ($parentClassReflection !== null) { if ($parentClassReflection->hasStaticProperty($name)) { - if ($scope->canReadProperty($parentClassReflection->getStaticProperty($name, $scope))) { + if ($scope->canReadProperty($parentClassReflection->getStaticProperty($name))) { return []; } return [ diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 4aea9fa21c..28eca1e677 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -398,7 +398,7 @@ public function getUnresolvedStaticPropertyPrototype(string $propertyName, Class throw new ClassNotFoundException($this->className); } - $property = $nakedClassReflection->getStaticProperty($propertyName, $scope); + $property = $nakedClassReflection->getStaticProperty($propertyName); $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); $resolvedClassReflection = null; diff --git a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php index 5db5d1304c..936d56b4aa 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -124,7 +124,7 @@ public function testDeprecatedAnnotations(bool $deprecated, string $className, ? } foreach ($deprecatedAnnotations['staticProperty'] ?? [] as $propertyName => $deprecatedMessage) { - $propertyAnnotation = $class->getStaticProperty($propertyName, $scope); + $propertyAnnotation = $class->getStaticProperty($propertyName); $this->assertSame($deprecated, $propertyAnnotation->isDeprecated()->yes()); $this->assertSame($deprecatedMessage, $propertyAnnotation->getDeprecatedDescription()); } diff --git a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php index fa6110785c..9e4e36901f 100644 --- a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php @@ -146,7 +146,7 @@ public function testInternalAnnotations(bool $internal, string $className, array } foreach ($internalAnnotations['staticProperty'] ?? [] as $propertyName) { - $propertyAnnotation = $class->getStaticProperty($propertyName, $scope); + $propertyAnnotation = $class->getStaticProperty($propertyName); $this->assertSame($internal, $propertyAnnotation->isInternal()->yes()); } From d67f402ae93a1a2a2aace959cc66e3302c381e95 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 20 Apr 2025 21:01:41 +0200 Subject: [PATCH 13/27] Solve todo --- .../Php/PhpClassReflectionExtension.php | 1 - src/Type/Accessory/HasPropertyType.php | 14 ++++++++------ src/Type/ObjectShapeType.php | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 6fb0f55388..9d16abe048 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -216,7 +216,6 @@ public function getNativeProperty(ClassReflection $classReflection, string $prop return $this->nativeProperties[$classReflection->getCacheKey()][$propertyName]; } - // TODO: Find the difference between createInstanceProperty and createStaticProperty private function createProperty( ClassReflection $classReflection, string $propertyName, diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index c6dd2be824..ba2dbf8892 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -72,8 +72,10 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - // TODO - return new IsSuperTypeOfResult($type->hasProperty($this->propertyName), []); + return new IsSuperTypeOfResult( + $type->hasInstanceProperty($this->propertyName)->or($type->hasStaticProperty($this->propertyName)), + [], + ); } public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult @@ -88,8 +90,10 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult $limit = IsSuperTypeOfResult::createMaybe(); } - // TODO - return $limit->and(new IsSuperTypeOfResult($otherType->hasProperty($this->propertyName), [])); + return $limit->and(new IsSuperTypeOfResult( + $otherType->hasInstanceProperty($this->propertyName)->or($otherType->hasStaticProperty($this->propertyName)), + [], + )); } public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult @@ -117,7 +121,6 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - // TODO public function hasInstanceProperty(string $propertyName): TrinaryLogic { if ($this->propertyName === $propertyName) { @@ -127,7 +130,6 @@ public function hasInstanceProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - // TODO public function hasStaticProperty(string $propertyName): TrinaryLogic { if ($this->propertyName === $propertyName) { diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index cc0aebf201..ee293e11b8 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -137,6 +137,21 @@ public function getUnresolvedInstancePropertyPrototype(string $propertyName, Cla ); } + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + throw new ShouldNotHappenException(); + } + + public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + throw new ShouldNotHappenException(); + } + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { From 3488a988706dc250da1eb16949378614a8009255 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 20 Apr 2025 21:25:55 +0200 Subject: [PATCH 14/27] Fix deprecation --- .../Deprecation/DeprecationProviderTest.php | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php b/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php index e88a5b66f6..2d177acc1e 100644 --- a/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php +++ b/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php @@ -117,29 +117,29 @@ public function testCustomDeprecations(): void self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getConstant('FOO')->getDeprecatedDescription()); // properties - self::assertFalse($notDeprecatedClass->getProperty('foo', $scopeForNotDeprecatedClass)->isDeprecated()->yes()); - self::assertNull($notDeprecatedClass->getProperty('foo', $scopeForNotDeprecatedClass)->getDeprecatedDescription()); + self::assertFalse($notDeprecatedClass->getInstanceProperty('foo', $scopeForNotDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($notDeprecatedClass->getInstanceProperty('foo', $scopeForNotDeprecatedClass)->getDeprecatedDescription()); - self::assertTrue($attributeDeprecatedClass->getProperty('foo', $scopeForDeprecatedClass)->isDeprecated()->yes()); - self::assertNull($attributeDeprecatedClass->getProperty('foo', $scopeForDeprecatedClass)->getDeprecatedDescription()); + self::assertTrue($attributeDeprecatedClass->getInstanceProperty('foo', $scopeForDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($attributeDeprecatedClass->getInstanceProperty('foo', $scopeForDeprecatedClass)->getDeprecatedDescription()); - self::assertTrue($phpDocDeprecatedClass->getProperty('foo', $scopeForPhpDocDeprecatedClass)->isDeprecated()->yes()); - self::assertNull($phpDocDeprecatedClass->getProperty('foo', $scopeForPhpDocDeprecatedClass)->getDeprecatedDescription()); + self::assertTrue($phpDocDeprecatedClass->getInstanceProperty('foo', $scopeForPhpDocDeprecatedClass)->isDeprecated()->yes()); + self::assertNull($phpDocDeprecatedClass->getInstanceProperty('foo', $scopeForPhpDocDeprecatedClass)->getDeprecatedDescription()); - self::assertTrue($phpDocDeprecatedClassWithMessages->getProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->isDeprecated()->yes()); - self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->getDeprecatedDescription()); + self::assertTrue($phpDocDeprecatedClassWithMessages->getInstanceProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('phpdoc', $phpDocDeprecatedClassWithMessages->getInstanceProperty('foo', $scopeForPhpDocDeprecatedClassWithMessages)->getDeprecatedDescription()); - self::assertTrue($attributeDeprecatedClassWithMessages->getProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->isDeprecated()->yes()); - self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->getDeprecatedDescription()); + self::assertTrue($attributeDeprecatedClassWithMessages->getInstanceProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->isDeprecated()->yes()); + self::assertSame('attribute', $attributeDeprecatedClassWithMessages->getInstanceProperty('foo', $scopeForAttributeDeprecatedClassWithMessages)->getDeprecatedDescription()); - self::assertTrue($doubleDeprecatedClass->getProperty('foo', $scopeForDoubleDeprecatedClass)->isDeprecated()->yes()); - self::assertSame('attribute', $doubleDeprecatedClass->getProperty('foo', $scopeForDoubleDeprecatedClass)->getDeprecatedDescription()); + self::assertTrue($doubleDeprecatedClass->getInstanceProperty('foo', $scopeForDoubleDeprecatedClass)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClass->getInstanceProperty('foo', $scopeForDoubleDeprecatedClass)->getDeprecatedDescription()); - self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->isDeprecated()->yes()); - self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->getDeprecatedDescription()); + self::assertTrue($doubleDeprecatedClassOnlyPhpDocMessage->getInstanceProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->isDeprecated()->yes()); + self::assertNull($doubleDeprecatedClassOnlyPhpDocMessage->getInstanceProperty('foo', $scopeForDoubleDeprecatedClassOnlyNativeMessage)->getDeprecatedDescription()); - self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->isDeprecated()->yes()); - self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->getDeprecatedDescription()); + self::assertTrue($doubleDeprecatedClassOnlyAttributeMessage->getInstanceProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->isDeprecated()->yes()); + self::assertSame('attribute', $doubleDeprecatedClassOnlyAttributeMessage->getInstanceProperty('foo', $scopeForDoubleDeprecatedClassOnlyCustomMessage)->getDeprecatedDescription()); // methods self::assertFalse($notDeprecatedClass->getMethod('foo', $scopeForNotDeprecatedClass)->isDeprecated()->yes()); From 756ca71e995c573de1587abff2eac14ae17c2430 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 13 May 2025 22:22:25 +0200 Subject: [PATCH 15/27] Fix --- src/Reflection/ClassReflection.php | 6 +++--- src/Type/MixedType.php | 4 ++-- src/Type/Traits/MaybeObjectTypeTrait.php | 4 ++-- src/Type/Traits/ObjectTypeTrait.php | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 56c249fb27..1e9a7b33fe 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -738,7 +738,7 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco // For BC purpose if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { - $property = $this->wrapExtendedProperty($this->getPhpExtension()->getStaticProperty($this, $propertyName)); + $property = $this->wrapExtendedProperty($propertyName, $this->getPhpExtension()->getStaticProperty($this, $propertyName)); if ($scope->canReadProperty($property)) { return $this->properties[$key] = $property; } @@ -780,7 +780,7 @@ public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswe continue; } - $property = $this->wrapExtendedProperty($extension->getProperty($this, $propertyName)); + $property = $this->wrapExtendedProperty($propertyName, $extension->getProperty($this, $propertyName)); if ($scope->canReadProperty($property)) { return $this->instanceProperties[$key] = $property; } @@ -810,7 +810,7 @@ public function getStaticProperty(string $propertyName): ExtendedPropertyReflect } if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { - $property = $this->wrapExtendedProperty($this->getPhpExtension()->getStaticProperty($this, $propertyName)); + $property = $this->wrapExtendedProperty($propertyName, $this->getPhpExtension()->getStaticProperty($this, $propertyName)); return $this->staticProperties[$key] = $property; } diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 31df320708..ad8d4fbfb1 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -434,7 +434,7 @@ public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswe public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -455,7 +455,7 @@ public function getStaticProperty(string $propertyName, ClassMemberAccessAnswere public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), diff --git a/src/Type/Traits/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index d919a587f4..71e4df3421 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -73,7 +73,7 @@ public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswe public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -94,7 +94,7 @@ public function getStaticProperty(string $propertyName, ClassMemberAccessAnswere public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 84e16c32ea..51a4922f43 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -84,7 +84,7 @@ public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswe public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), @@ -105,7 +105,7 @@ public function getStaticProperty(string $propertyName, ClassMemberAccessAnswere public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - $property = new DummyPropertyReflection(); + $property = new DummyPropertyReflection($propertyName); return new CallbackUnresolvedPropertyPrototypeReflection( $property, $property->getDeclaringClass(), From b3997ba40328dba1e4754a751dc7564b3be79431 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 14 May 2025 10:15:22 +0200 Subject: [PATCH 16/27] Fix deprecation --- src/Rules/Properties/AccessStaticPropertiesRule.php | 4 ++-- src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php | 4 ++-- .../RestrictedUsage/RestrictedStaticPropertyUsageRule.php | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 19caf0e08b..e85747e84d 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -138,8 +138,8 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $locationData = []; $locationClassReflection = $this->reflectionProvider->getClass($class); - if ($locationClassReflection->hasProperty($name)) { - $locationData['property'] = $locationClassReflection->getProperty($name, $scope); + if ($locationClassReflection->hasStaticProperty($name)) { + $locationData['property'] = $locationClassReflection->getStaticProperty($name); } $messages = $this->classCheck->checkClassNames( diff --git a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php index 75163a38a7..5136a304ee 100644 --- a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php @@ -57,11 +57,11 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$classReflection->hasProperty($propertyName)) { + if (!$classReflection->hasInstanceProperty($propertyName)) { continue; } - $propertyReflection = $classReflection->getProperty($propertyName, $scope); + $propertyReflection = $classReflection->getInstanceProperty($propertyName, $scope); foreach ($extensions as $extension) { $restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $scope); if ($restrictedUsage === null) { diff --git a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php index 5d45cb56a6..6361276a20 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->class, '', // We don't care about the error message - static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($propertyName)->yes(), + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasInstanceProperty($propertyName)->yes(), ); if ($classTypeResult->getType() instanceof ErrorType) { @@ -77,11 +77,11 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$classReflection->hasProperty($propertyName)) { + if (!$classReflection->hasStaticProperty($propertyName)) { continue; } - $propertyReflection = $classReflection->getProperty($propertyName, $scope); + $propertyReflection = $classReflection->getStaticProperty($propertyName); foreach ($extensions as $extension) { $restrictedUsage = $extension->isRestrictedPropertyUsage($propertyReflection, $scope); if ($restrictedUsage === null) { From a9d4705281c3e87a036db2d97cc47faa2eec7162 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 14 May 2025 10:37:06 +0200 Subject: [PATCH 17/27] Fix --- src/Rules/Properties/AccessPropertiesCheck.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index 15daac4da2..b25d166071 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -101,7 +101,9 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $scope, NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), - static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasInstanceProperty($name)->yes(), + static fn (Type $type): bool => $type->canAccessProperties()->yes() && ( + $type->hasInstanceProperty($name)->yes() || $type->hasStaticProperty($name)->yes() + ), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { From f7a240e4b595d9a88c1ed778574dcda6258d1ab6 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 17 Jul 2025 20:35:53 +0200 Subject: [PATCH 18/27] Fix --- src/Type/ObjectType.php | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 28eca1e677..de1e120173 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -261,7 +261,8 @@ public function hasInstanceProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - if ($classReflection->hasInstanceProperty($propertyName)) { + $classHasProperty = RecursionGuard::run($this, static fn (): bool => $classReflection->hasInstanceProperty($propertyName)); + if ($classHasProperty === true || $classHasProperty instanceof ErrorType) { return TrinaryLogic::createYes(); } @@ -327,7 +328,17 @@ public function getUnresolvedInstancePropertyPrototype(string $propertyName, Cla throw new ClassNotFoundException($this->className); } - $property = $nakedClassReflection->getInstanceProperty($propertyName, $scope); + $property = RecursionGuard::run($this, static fn () => $nakedClassReflection->getInstanceProperty($propertyName, $scope)); + if ($property instanceof ErrorType) { + $property = new DummyPropertyReflection($propertyName); + + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); $resolvedClassReflection = null; @@ -356,7 +367,8 @@ public function hasStaticProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - if ($classReflection->hasStaticProperty($propertyName)) { + $classHasProperty = RecursionGuard::run($this, static fn (): bool => $classReflection->hasStaticProperty($propertyName)); + if ($classHasProperty === true || $classHasProperty instanceof ErrorType) { return TrinaryLogic::createYes(); } @@ -398,7 +410,17 @@ public function getUnresolvedStaticPropertyPrototype(string $propertyName, Class throw new ClassNotFoundException($this->className); } - $property = $nakedClassReflection->getStaticProperty($propertyName); + $property = RecursionGuard::run($this, static fn () => $nakedClassReflection->getStaticProperty($propertyName)); + if ($property instanceof ErrorType) { + $property = new DummyPropertyReflection($propertyName); + + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); $resolvedClassReflection = null; From ed7a97ecc4fa80ee582417ee2f2621a7cd0242bd Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 30 Aug 2025 11:55:22 +0200 Subject: [PATCH 19/27] Fix --- src/Type/ObjectShapeType.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index ee293e11b8..351e5000c7 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -185,6 +185,12 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult ), ], ); + if (!$hasProperty->yes() && $type->hasStaticProperty($propertyName)->yes()) { + $result = $result->and(new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is static.', $type->getStaticProperty($propertyName, $scope)->getDeclaringClass()->getDisplayName(), $propertyName), + ])); + continue; + } if ($hasProperty->no()) { if (in_array($propertyName, $this->optionalProperties, true)) { continue; @@ -202,12 +208,6 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult $hasProperty = AcceptsResult::createYes(); } - if (!$hasProperty->yes() && $type->hasStaticProperty($propertyName)->yes()) { - return new AcceptsResult(TrinaryLogic::createNo(), [ - sprintf('Property %s::$%s is static.', $type->getStaticProperty($propertyName, $scope)->getDeclaringClass()->getDisplayName(), $propertyName), - ]); - } - $result = $result->and($hasProperty); try { $otherProperty = $type->getInstanceProperty((string) $propertyName, $scope); From 422a095b082a272fa79fb931c0ba256ddc00455a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 10 Sep 2025 16:03:51 +0200 Subject: [PATCH 20/27] Fix --- src/Reflection/ClassReflection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 1e9a7b33fe..297b84887f 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -506,7 +506,7 @@ public function hasInstanceProperty(string $propertyName): bool } foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { - if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { + if ($i > 0 && !$this->allowsDynamicProperties()) { break; } if ($extension->hasProperty($this, $propertyName)) { @@ -772,7 +772,7 @@ public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswe if (!isset($this->instanceProperties[$key])) { foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { - if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { + if ($i > 0 && !$this->allowsDynamicProperties()) { break; } From 44f79f20815989f6f0c223dfb7468563555b3299 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 10 Sep 2025 16:21:21 +0200 Subject: [PATCH 21/27] Do not use annotation for static properties --- src/Reflection/Php/PhpClassReflectionExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 9d16abe048..3c004e050e 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -195,7 +195,7 @@ public function hasStaticProperty(ClassReflection $classReflection, string $prop public function getStaticProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection { if (!isset($this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName])) { - $this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName] = $this->createProperty($classReflection, $propertyName, true); + $this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName] = $this->createProperty($classReflection, $propertyName, false); } return $this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName]; From 1539df8594dda6816b6b3d160683d47e90d5936c Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 10 Sep 2025 16:40:41 +0200 Subject: [PATCH 22/27] Fix rebase --- src/Rules/Properties/AccessPropertiesCheck.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index b25d166071..70f96a089a 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -274,7 +274,7 @@ private function pickProperty(Scope $scope, Type $type, string $name): ?Extended $types = []; if ($type instanceof UnionType) { foreach ($type->getTypes() as $innerType) { - if ($innerType->hasProperty($name)->no()) { + if ($innerType->hasInstanceProperty($name)->no()) { continue; } @@ -284,7 +284,7 @@ private function pickProperty(Scope $scope, Type $type, string $name): ?Extended if (count($types) === 0) { try { - return $type->getProperty($name, $scope); + return $type->getInstanceProperty($name, $scope); } catch (MissingPropertyFromReflectionException) { return null; } @@ -292,7 +292,7 @@ private function pickProperty(Scope $scope, Type $type, string $name): ?Extended if (count($types) === 1) { try { - return $types[0]->getProperty($name, $scope); + return $types[0]->getInstanceProperty($name, $scope); } catch (MissingPropertyFromReflectionException) { return null; } @@ -301,7 +301,7 @@ private function pickProperty(Scope $scope, Type $type, string $name): ?Extended $unionType = TypeCombinator::union(...$types); try { - return $unionType->getProperty($name, $scope); + return $unionType->getInstanceProperty($name, $scope); } catch (MissingPropertyFromReflectionException) { return null; } From b89dc119d6679e1b6ec37d524499957723fdbcaf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 10 Sep 2025 16:50:25 +0200 Subject: [PATCH 23/27] Fix cs --- ...uireExtendsPropertiesClassReflectionExtension.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php index 27b51f7fdd..a4971b772f 100644 --- a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php +++ b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php @@ -19,7 +19,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa $classReflection, $propertyName, static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()), ) !== null; } @@ -30,7 +30,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa $classReflection, $propertyName, static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasProperty($propertyName), - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getProperty($propertyName, new OutOfClassScope()), ); if ($property === null) { throw new ShouldNotHappenException(); @@ -45,7 +45,7 @@ public function hasInstanceProperty(ClassReflection $classReflection, string $pr $classReflection, $propertyName, static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasInstanceProperty($propertyName), - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()), ) !== null; } @@ -55,7 +55,7 @@ public function getInstanceProperty(ClassReflection $classReflection, string $pr $classReflection, $propertyName, static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasInstanceProperty($propertyName), - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()), ); if ($property === null) { throw new ShouldNotHappenException(); @@ -70,7 +70,7 @@ public function hasStaticProperty(ClassReflection $classReflection, string $prop $classReflection, $propertyName, static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasStaticProperty($propertyName), - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()), ) !== null; } @@ -80,7 +80,7 @@ public function getStaticProperty(ClassReflection $classReflection, string $prop $classReflection, $propertyName, static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasStaticProperty($propertyName), - static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()) + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()), ); if ($property === null) { throw new ShouldNotHappenException(); From 5297d77b5074ec1aa2f6cf4cbb4dff4eb3089a97 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 10 Sep 2025 17:21:12 +0200 Subject: [PATCH 24/27] Fix --- .../Properties/AccessPropertiesCheck.php | 5 ++-- .../AccessStaticPropertiesRuleTest.php | 22 ++++++++++++++++ .../Rules/Properties/data/bug-8668-bis.php | 25 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-8668-bis.php diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index 70f96a089a..82d1753a1d 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -130,7 +130,8 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string } $has = $type->hasInstanceProperty($name); - if ($has->maybe()) { + $hasStatic = $type->hasStaticProperty($name); + if ($has->maybe() && !$hasStatic->yes()) { if ($scope->isUndefinedExpressionAllowed($node)) { if (!$this->checkDynamicProperties) { return []; @@ -202,7 +203,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string } } - if ($type->hasStaticProperty($name)->yes()) { + if ($hasStatic->yes()) { return [ RuleErrorBuilder::message(sprintf( 'Non-static access to static property %s::$%s.', diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 785e833194..f15d03c7ad 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -317,4 +317,26 @@ public function testBug12775(): void $this->analyse([__DIR__ . '/data/bug-12775.php'], []); } + public function testBug8668Bis(): void + { + $this->analyse([__DIR__ . '/data/bug-8668-bis.php'], [ + [ + 'Static access to instance property Bug8668Bis\Sample::$sample.', + 9, + ], + [ + 'Static access to instance property Bug8668Bis\Sample::$sample.', + 10, + ], + [ + 'Static access to instance property Bug8668Bis\Sample2::$sample.', + 20, + ], + [ + 'Static access to instance property Bug8668Bis\Sample2::$sample.', + 21, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-8668-bis.php b/tests/PHPStan/Rules/Properties/data/bug-8668-bis.php new file mode 100644 index 0000000000..fd561672ba --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8668-bis.php @@ -0,0 +1,25 @@ +sample; // ok + } +} + +class Sample2 { + private $sample = 'abc'; + + public function test(): void { + echo self::$sample; + echo isset(self::$sample); + + echo $this->sample; // ok + } +} From 27bfdcc732e0cea7e324c257361139bd5ea84841 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Sep 2025 12:58:18 +0200 Subject: [PATCH 25/27] Refactor stuff I disagree with --- src/Reflection/ClassReflection.php | 46 +++++++++++++------ .../Php/PhpClassReflectionExtension.php | 41 +---------------- 2 files changed, 33 insertions(+), 54 deletions(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 297b84887f..445c69d92a 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -484,8 +484,11 @@ public function hasProperty(string $propertyName): bool } // For BC purpose - if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { - return $this->hasPropertyCache[$propertyName] = true; + if ($this->getPhpExtension()->hasProperty($this, $propertyName)) { + $property = $this->getPhpExtension()->getProperty($this, $propertyName); + if ($property->isStatic()) { + return $this->hasPropertyCache[$propertyName] = true; + } } if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { @@ -527,8 +530,11 @@ public function hasStaticProperty(string $propertyName): bool return $this->hasStaticPropertyCache[$propertyName]; } - if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { - return $this->hasStaticPropertyCache[$propertyName] = true; + if ($this->getPhpExtension()->hasProperty($this, $propertyName)) { + $property = $this->getPhpExtension()->getProperty($this, $propertyName); + if ($property->isStatic()) { + return $this->hasStaticPropertyCache[$propertyName] = true; + } } if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { @@ -737,12 +743,14 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco } // For BC purpose - if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { - $property = $this->wrapExtendedProperty($propertyName, $this->getPhpExtension()->getStaticProperty($this, $propertyName)); - if ($scope->canReadProperty($property)) { - return $this->properties[$key] = $property; + if ($this->getPhpExtension()->hasProperty($this, $propertyName)) { + $property = $this->wrapExtendedProperty($propertyName, $this->getPhpExtension()->getProperty($this, $propertyName)); + if ($property->isStatic()) { + if ($scope->canReadProperty($property)) { + return $this->properties[$key] = $property; + } + $this->properties[$key] = $property; } - $this->properties[$key] = $property; } if (!isset($this->properties[$key])) { @@ -780,7 +788,12 @@ public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswe continue; } - $property = $this->wrapExtendedProperty($propertyName, $extension->getProperty($this, $propertyName)); + $nakedProperty = $extension->getProperty($this, $propertyName); + if ($nakedProperty->isStatic()) { + continue; + } + + $property = $this->wrapExtendedProperty($propertyName, $nakedProperty); if ($scope->canReadProperty($property)) { return $this->instanceProperties[$key] = $property; } @@ -809,9 +822,14 @@ public function getStaticProperty(string $propertyName): ExtendedPropertyReflect return $this->staticProperties[$key]; } - if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { - $property = $this->wrapExtendedProperty($propertyName, $this->getPhpExtension()->getStaticProperty($this, $propertyName)); - return $this->staticProperties[$key] = $property; + if ($this->getPhpExtension()->hasProperty($this, $propertyName)) { + $nakedProperty = $this->getPhpExtension()->getProperty($this, $propertyName); + if ($nakedProperty->isStatic()) { + $property = $this->wrapExtendedProperty($propertyName, $nakedProperty); + if ($property->isStatic()) { + return $this->staticProperties[$key] = $property; + } + } } if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { @@ -824,7 +842,7 @@ public function getStaticProperty(string $propertyName): ExtendedPropertyReflect public function hasNativeProperty(string $propertyName): bool { - return $this->getPhpExtension()->hasNativeProperty($this, $propertyName); + return $this->getPhpExtension()->hasProperty($this, $propertyName); } public function getNativeProperty(string $propertyName): PhpPropertyReflection diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 3c004e050e..2970ef2f80 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -71,9 +71,6 @@ final class PhpClassReflectionExtension /** @var PhpPropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; - /** @var ExtendedPropertyReflection[][] */ - private array $staticPropertiesIncludingAnnotations = []; - /** @var PhpPropertyReflection[][] */ private array $nativeProperties = []; @@ -121,17 +118,6 @@ public function evictPrivateSymbols(string $classCacheKey): void unset($this->propertiesIncludingAnnotations[$key][$name]); } } - foreach ($this->staticPropertiesIncludingAnnotations as $key => $properties) { - if ($key !== $classCacheKey) { - continue; - } - foreach ($properties as $name => $property) { - if (!$property->isPrivate()) { - continue; - } - unset($this->staticPropertiesIncludingAnnotations[$key][$name]); - } - } foreach ($this->nativeProperties as $key => $properties) { if ($key !== $classCacheKey) { continue; @@ -169,10 +155,7 @@ public function evictPrivateSymbols(string $classCacheKey): void public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - $nativeReflection = $classReflection->getNativeReflection(); - - return $nativeReflection->hasProperty($propertyName) - && !$nativeReflection->getProperty($propertyName)->isStatic(); + return $classReflection->getNativeReflection()->hasProperty($propertyName); } public function getProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection @@ -184,28 +167,6 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa return $this->propertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName]; } - public function hasStaticProperty(ClassReflection $classReflection, string $propertyName): bool - { - $nativeReflection = $classReflection->getNativeReflection(); - - return $nativeReflection->hasProperty($propertyName) - && $nativeReflection->getProperty($propertyName)->isStatic(); - } - - public function getStaticProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection - { - if (!isset($this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName])) { - $this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName] = $this->createProperty($classReflection, $propertyName, false); - } - - return $this->staticPropertiesIncludingAnnotations[$classReflection->getCacheKey()][$propertyName]; - } - - public function hasNativeProperty(ClassReflection $classReflection, string $propertyName): bool - { - return $classReflection->getNativeReflection()->hasProperty($propertyName); - } - public function getNativeProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection { if (!isset($this->nativeProperties[$classReflection->getCacheKey()][$propertyName])) { From d0bf6b1fb30b95533d53505f72859c0d887e7ccf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 14 Sep 2025 09:23:29 +0200 Subject: [PATCH 26/27] Fix CS --- src/Type/ObjectShapeType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index 351e5000c7..bf1ddc1c92 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -296,7 +296,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult $result = IsSuperTypeOfResult::createYes(); $scope = new OutOfClassScope(); foreach ($this->properties as $propertyName => $propertyType) { - $hasProperty = new IsSuperTypeOfResult($type->hasInstanceProperty((string)$propertyName), []); + $hasProperty = new IsSuperTypeOfResult($type->hasInstanceProperty((string) $propertyName), []); if ($hasProperty->no()) { if (in_array($propertyName, $this->optionalProperties, true)) { continue; From 0fa5d97765dc47adfc4a56d9280ee385ec729541 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 14 Sep 2025 09:24:23 +0200 Subject: [PATCH 27/27] Fix --- src/Reflection/ClassReflection.php | 19 ++++++++----------- src/Type/ObjectShapeType.php | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 445c69d92a..4c94093969 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -485,10 +485,7 @@ public function hasProperty(string $propertyName): bool // For BC purpose if ($this->getPhpExtension()->hasProperty($this, $propertyName)) { - $property = $this->getPhpExtension()->getProperty($this, $propertyName); - if ($property->isStatic()) { - return $this->hasPropertyCache[$propertyName] = true; - } + return $this->hasPropertyCache[$propertyName] = true; } if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { @@ -513,6 +510,10 @@ public function hasInstanceProperty(string $propertyName): bool break; } if ($extension->hasProperty($this, $propertyName)) { + $property = $extension->getProperty($this, $propertyName); + if ($property->isStatic()) { + continue; + } return $this->hasInstancePropertyCache[$propertyName] = true; } } @@ -744,13 +745,9 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco // For BC purpose if ($this->getPhpExtension()->hasProperty($this, $propertyName)) { - $property = $this->wrapExtendedProperty($propertyName, $this->getPhpExtension()->getProperty($this, $propertyName)); - if ($property->isStatic()) { - if ($scope->canReadProperty($property)) { - return $this->properties[$key] = $property; - } - $this->properties[$key] = $property; - } + $property = $this->getPhpExtension()->getProperty($this, $propertyName); + + return $this->properties[$key] = $property; } if (!isset($this->properties[$key])) { diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index bf1ddc1c92..6e832f5c22 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -185,9 +185,9 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult ), ], ); - if (!$hasProperty->yes() && $type->hasStaticProperty($propertyName)->yes()) { + if (!$hasProperty->yes() && $type->hasStaticProperty((string) $propertyName)->yes()) { $result = $result->and(new AcceptsResult(TrinaryLogic::createNo(), [ - sprintf('Property %s::$%s is static.', $type->getStaticProperty($propertyName, $scope)->getDeclaringClass()->getDisplayName(), $propertyName), + sprintf('Property %s::$%s is static.', $type->getStaticProperty((string) $propertyName, $scope)->getDeclaringClass()->getDisplayName(), $propertyName), ])); continue; }