diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 16a9c15f05..ce7b62cb17 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4287,7 +4287,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; } @@ -6127,7 +6127,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) { @@ -6140,12 +6143,43 @@ 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 */ 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 b43f0298cc..7d41d88d90 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3084,7 +3084,7 @@ static function (): 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)) { @@ -5627,8 +5627,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), $scope); if ($propertyReflection->canChangeTypeAfterAssignment()) { @@ -5716,7 +5716,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), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 1134614b2f..c817f9e1a4 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/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 77a4f957fe..3c32803347 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -253,7 +253,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); } @@ -369,13 +369,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()); $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); } diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index ec4dcba592..d114f34914 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -198,7 +198,7 @@ public function getUninitializedProperties( continue; } - $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName); + $propertyReflection = $usageScope->getInstancePropertyReflection($fetchedOnType, $propertyName); if ($propertyReflection === null) { continue; } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index f6e3da43e9..b3fdb56be0 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -82,6 +82,12 @@ final class ClassReflection /** @var ExtendedPropertyReflection[] */ private array $properties = []; + /** @var ExtendedPropertyReflection[] */ + private array $instanceProperties = []; + + /** @var ExtendedPropertyReflection[] */ + private array $staticProperties = []; + /** @var RealClassClassConstantReflection[] */ private array $constants = []; @@ -148,6 +154,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 @@ -462,6 +474,9 @@ private function allowsDynamicPropertiesExtensions(): bool return false; } + /** + * @deprecated Use hasInstanceProperty or hasStaticProperty instead + */ public function hasProperty(string $propertyName): bool { if (array_key_exists($propertyName, $this->hasPropertyCache)) { @@ -481,6 +496,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; } @@ -488,6 +508,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)) { @@ -632,6 +695,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; @@ -642,6 +719,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()) { @@ -671,6 +749,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); @@ -685,9 +772,72 @@ 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): ExtendedPropertyReflection + { + $key = $propertyName; + if (isset($this->staticProperties[$key])) { + return $this->staticProperties[$key]; + } + + if ($this->getPhpExtension()->hasStaticProperty($this, $propertyName)) { + $property = $this->wrapExtendedProperty($this->getPhpExtension()->getStaticProperty($this, $propertyName)); + return $this->staticProperties[$key] = $property; + } + + if ($this->requireExtendsPropertiesClassReflectionExtension->hasStaticProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getStaticProperty($this, $propertyName); + return $this->staticProperties[$key] = $property; + } + + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); + } + 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/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index fb935ac630..d74125e5bd 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -403,11 +403,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/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php index 4b21f92451..78b94e4d69 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/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index fc0dd70dbe..d9463b6dc8 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -72,6 +72,9 @@ final class PhpClassReflectionExtension /** @var ExtendedPropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; + /** @var ExtendedPropertyReflection[][] */ + private array $staticPropertiesIncludingAnnotations = []; + /** @var PhpPropertyReflection[][] */ private array $nativeProperties = []; @@ -119,6 +122,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; @@ -156,7 +170,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): ExtendedPropertyReflection @@ -168,6 +185,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])) { diff --git a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php index 550a7bee59..27b51f7fdd 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,66 @@ 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, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasInstanceProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getInstanceProperty($propertyName, new OutOfClassScope()) + ) !== null; + } + + public function getInstanceProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty( + $classReflection, + $propertyName, + 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(); + } + + return $property; + } + + public function hasStaticProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findProperty( + $classReflection, + $propertyName, + static fn (Type $type, string $propertyName): TrinaryLogic => $type->hasStaticProperty($propertyName), + static fn (Type $type, string $propertyName): ExtendedPropertyReflection => $type->getStaticProperty($propertyName, new OutOfClassScope()) + ) !== null; + } + + public function getStaticProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty( + $classReflection, + $propertyName, + 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(); + } + + 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 +108,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/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index 239e732056..ed17441c78 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -176,7 +176,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/AccessPrivatePropertyThroughStaticRule.php b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php index 548e176c1d..70b1242690 100644 --- a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -36,17 +36,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/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index 467cf98a99..c8664dec8f 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -67,7 +67,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)), - 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) { @@ -93,7 +93,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ]; } - $has = $type->hasProperty($name); + $has = $type->hasInstanceProperty($name); if (!$has->no() && $this->canAccessUndefinedProperties($scope, $node)) { return []; } @@ -124,12 +124,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 []; } @@ -157,6 +157,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()), @@ -173,17 +183,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ]; } - $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 80f1034120..ad5f8092ea 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -140,7 +140,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)), - 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) { @@ -172,7 +172,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]); } - $has = $classType->hasProperty($name); + $has = $classType->hasStaticProperty($name); if (!$has->no() && $scope->isUndefinedExpressionAllowed($node)) { return []; } @@ -188,8 +188,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))) { return []; } return [ @@ -205,33 +205,33 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, } } - return array_merge($messages, [ - RuleErrorBuilder::message(sprintf( - 'Access to an undefined static property %s::$%s.', - $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $name, - ))->identifier('staticProperty.notFound')->build(), - ]); - } - - $property = $classType->getProperty($name, $scope); - if (!$property->isStatic()) { - $hasPropertyTypes = TypeUtils::getHasPropertyTypes($classType); - foreach ($hasPropertyTypes as $hasPropertyType) { - if ($hasPropertyType->getPropertyName() === $name) { - return []; + 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( - 'Static access to instance property %s::$%s.', - $property->getDeclaringClass()->getDisplayName(), + 'Access to an undefined static property %s::$%s.', + $typeForDescribe->describe(VerbosityLevel::typeOnly()), $name, - ))->identifier('property.staticAccess')->build(), + ))->identifier('staticProperty.notFound')->build(), ]); } + $property = $classType->getStaticProperty($name, $scope); if (!$scope->canReadProperty($property)) { return array_merge($messages, [ RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Properties/PropertyReflectionFinder.php b/src/Rules/Properties/PropertyReflectionFinder.php index 67b2785fa9..809a15b1a0 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -31,7 +31,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( @@ -63,7 +63,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( @@ -89,13 +89,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; @@ -111,16 +111,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/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 71b6b42759..07dc069628 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -72,7 +72,10 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { - 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 @@ -87,7 +90,10 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult $limit = IsSuperTypeOfResult::createMaybe(); } - 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 @@ -120,6 +126,24 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + if ($this->propertyName === $propertyName) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createMaybe(); + } + + 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/ClosureType.php b/src/Type/ClosureType.php index 9a12b00fbb..37adc98017 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -315,6 +315,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 e3ae5e23f5..cfc0513612 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 149536a573..1ea13f645a 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -533,6 +533,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->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 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 487a474827..ada8a8beae 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -405,6 +405,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 518ffa8f4a..344d2950ed 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 1a1beed6c0..c66897d79d 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,21 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } + 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) { @@ -143,7 +173,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult $result = AcceptsResult::createYes(); $scope = new OutOfClassScope(); foreach ($this->properties as $propertyName => $propertyType) { - $typeHasProperty = $type->hasProperty($propertyName); + $typeHasProperty = $type->hasInstanceProperty($propertyName); $hasProperty = new AcceptsResult( $typeHasProperty, $typeHasProperty->yes() ? [] : [ @@ -159,16 +189,23 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult if (in_array($propertyName, $this->optionalProperties, true)) { continue; } + return $hasProperty; } if ($hasProperty->maybe() && in_array($propertyName, $this->optionalProperties, true)) { $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->getProperty($propertyName, $scope); + $otherProperty = $type->getInstanceProperty($propertyName, $scope); } catch (MissingPropertyFromReflectionException) { return new AcceptsResult( $result->result, @@ -255,7 +292,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult $result = IsSuperTypeOfResult::createYes(); $scope = new OutOfClassScope(); foreach ($this->properties as $propertyName => $propertyType) { - $hasProperty = new IsSuperTypeOfResult($type->hasProperty($propertyName), []); + $hasProperty = new IsSuperTypeOfResult($type->hasInstanceProperty($propertyName), []); if ($hasProperty->no()) { if (in_array($propertyName, $this->optionalProperties, true)) { continue; @@ -269,7 +306,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult $result = $result->and($hasProperty); try { - $otherProperty = $type->getProperty($propertyName, $scope); + $otherProperty = $type->getInstanceProperty($propertyName, $scope); } catch (MissingPropertyFromReflectionException) { return $result; } @@ -366,12 +403,12 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $typeMap = TemplateTypeMap::createEmpty(); $scope = new OutOfClassScope(); foreach ($this->properties as $name => $propertyType) { - if ($receivedType->hasProperty($name)->no()) { + if ($receivedType->hasInstanceProperty($name)->no()) { continue; } try { - $receivedProperty = $receivedType->getProperty($name, $scope); + $receivedProperty = $receivedType->getInstanceProperty($name, $scope); } catch (MissingPropertyFromReflectionException) { continue; } @@ -462,10 +499,10 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $scope = new OutOfClassScope(); foreach ($this->properties as $name => $propertyType) { - if (!$right->hasProperty($name)->yes()) { + if (!$right->hasInstanceProperty($name)->yes()) { return $this; } - $transformed = $cb($propertyType, $right->getProperty($name, $scope)->getReadableType()); + $transformed = $cb($propertyType, $right->getInstanceProperty($name, $scope)->getReadableType()); if ($transformed !== $propertyType) { $stillOriginal = false; } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index e5b2540d7b..e0bf42a831 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -22,7 +22,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\CalledOnTypeUnresolvedMethodPrototypeReflection; @@ -97,6 +96,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 = []; @@ -127,6 +132,8 @@ public static function resetCaches(): void self::$superTypes = []; self::$methods = []; self::$properties = []; + self::$instanceProperties = []; + self::$staticProperties = []; self::$ancestors = []; self::$enumCases = []; } @@ -246,22 +253,170 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember ); } - public function getPropertyWithoutTransformingStatic(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function hasInstanceProperty(string $propertyName): TrinaryLogic { - $classReflection = $this->getNakedClassReflection(); + $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 (!$classReflection->hasProperty($propertyName)) { - $classReflection = $this->getClassReflection(); + if ($nakedClassReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($propertyName === 'value' && $nakedClassReflection->isBackedEnum()) + ) { + $properties = []; + foreach ($this->getEnumCases() as $enumCase) { + $properties[] = $enumCase->getUnresolvedInstancePropertyPrototype($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); } - return $classReflection->getProperty($propertyName, $scope); + $property = $nakedClassReflection->getStaticProperty($propertyName); + + $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, + ); } public function getReferencedClasses(): array diff --git a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php index 51d8999c2f..d627cde8c4 100644 --- a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -163,7 +163,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 $allowMaybe ? new MixedType() : null; } @@ -171,7 +171,7 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $ continue; } - $returnTypes[] = $type->getProperty($propertyName, $scope)->getReadableType(); + $returnTypes[] = $type->getInstanceProperty($propertyName, $scope)->getReadableType(); } } diff --git a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php index b988a7883a..05bdd01dc7 100644 --- a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php @@ -40,7 +40,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/StaticType.php b/src/Type/StaticType.php index 9db20e5b34..9a21560777 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -237,6 +237,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 5eb703077f..335053c39a 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()->getUnresolvedStaticPropertyPrototype($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 ff50c721d3..537f1d6b91 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->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::createMaybe(); + } + + 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::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 45fc20121f..40535c4018 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->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::createMaybe(); + } + + 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/Type.php b/src/Type/Type.php index 15886a053c..a2da132974 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 08d678152a..6967350d98 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -490,6 +490,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/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 167cad5f8e..a6716b2ee0 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -9118,10 +9118,6 @@ public 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); } diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php index 35d8075f8b..d54337a07a 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php @@ -287,11 +287,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 48c4197868..0610a37549 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -38,6 +38,8 @@ public function dataDeprecatedAnnotations(): array ], 'property' => [ 'foo' => null, + ], + 'staticProperty' => [ 'staticFoo' => null, ], ], @@ -56,6 +58,8 @@ public function dataDeprecatedAnnotations(): array ], 'property' => [ 'deprecatedFoo' => null, + ], + 'staticProperty' => [ 'deprecatedStaticFoo' => null, ], ], @@ -112,7 +116,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); $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 d7af0d248f..749f719194 100644 --- a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php @@ -31,6 +31,8 @@ public function dataInternalAnnotations(): array ], 'property' => [ 'foo', + ], + 'staticProperty' => [ 'staticFoo', ], ], @@ -48,6 +50,8 @@ public function dataInternalAnnotations(): array ], 'property' => [ 'internalFoo', + ], + 'staticProperty' => [ 'internalStaticFoo', ], ], @@ -88,6 +92,8 @@ public function dataInternalAnnotations(): array ], 'property' => [ 'foo', + ], + 'staticProperty' => [ 'staticFoo', ], ], @@ -102,6 +108,8 @@ public function dataInternalAnnotations(): array ], 'property' => [ 'internalFoo', + ], + 'staticProperty' => [ 'internalStaticFoo', ], ], @@ -132,7 +140,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); $this->assertSame($internal, $propertyAnnotation->isInternal()->yes()); } diff --git a/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php b/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php index c52796550d..ad5d59bab4 100644 --- a/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php +++ b/tests/PHPStan/Reflection/Deprecation/DeprecationProviderTest.php @@ -110,29 +110,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()); diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index e14d95f33e..8ade9becb8 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, ], ]); 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. diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 88834051bb..9bb5e645f7 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -310,4 +310,9 @@ public function testBug8333(): void ]); } + public function testBug12775(): void + { + $this->analyse([__DIR__ . '/data/bug-12775.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 8d050e7636..160ff6af82 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -43,10 +43,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, 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 @@ +hasProperty($propertyName); + $actualResult = $type->hasInstanceProperty($propertyName); $this->assertSame( $expectedResult->describe(), $actualResult->describe(),